[dev][operation] Add operation library This library provides a generic solution to the private section problem which exists for types such as usb_request_t, node_operation_t, block_opt_t, and others. Specialized wrappers for each of those types can be built on top this library. More specifically, the problem exists whereby a series of drivers reuse the same object as it traverses the driver stack for a specific subsystem. There exists a public section specified by a banjo protocol, along with a private section for each layer in driver stack appended to the end of it like so: --------------------- | Public Definition | --------------------- | Driver 1 Private | --------------------- | Driver 2 Private | --------------------- | ... | --------------------- | Driver N Private | --------------------- Driver N in this case would perform the allocation of the entire struct. Driver 1 in the example above would be the device driver which talks directly to hardware. The request would only be "owned" by a single driver at a time, but only Driver N (the one who allocated the request) would be allowed to free it. The library is inspired by the usb::Request and friends library. They will transition to being built on top of this library in future CLs. Tested: `runtests -t operation-test` Change-Id: I3ab6bdab14ef5f1654bde9817321286bf5e6eb36
diff --git a/system/dev/lib/operation/dummy.cpp b/system/dev/lib/operation/dummy.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/system/dev/lib/operation/dummy.cpp
diff --git a/system/dev/lib/operation/include/lib/operation/operation.h b/system/dev/lib/operation/include/lib/operation/operation.h new file mode 100644 index 0000000..90e5086 --- /dev/null +++ b/system/dev/lib/operation/include/lib/operation/operation.h
@@ -0,0 +1,506 @@ +// 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. + +#pragma once + +#include <optional> +#include <tuple> +#include <utility> + +#include <ddk/debug.h> +#include <fbl/algorithm.h> +#include <fbl/auto_lock.h> +#include <fbl/intrusive_double_list.h> +#include <fbl/mutex.h> +#include <fbl/unique_ptr.h> +#include <zircon/assert.h> +#include <zircon/compiler.h> + +namespace operation { + +// A problem exists whereby a series of drivers reuse the same object as it +// traverses the driver stack for a specific subsystem. There exists a public +// section specified by a banjo protocol, along with a private section for +// each layer in driver stack appended to the end of it like so: +// +// --------------------- +// | Public Definition | +// --------------------- +// | Driver 1 Private | +// --------------------- +// | Driver 2 Private | +// --------------------- +// | ... | +// --------------------- +// | Driver N Private | +// --------------------- +// +// Driver N in this case would perform the allocation of the entire struct. +// Driver 1 in the example above would be the device driver which talks +// directly to hardware. The request would only be "owned" by a single +// driver at a time, but only Driver N (the one who allocated the request) +// would be allowed to free it. +// +// This library provides a generic solution to the private section problem +// which exists for types such as usb_request_t, node_operation_t, +// block_op_t, and others. Specialized wrappers for each of those types +// can be built on top this library. +// +// operation::Operation and operation::UnownedOperation provide some additional +// safety to prevent leaks and out of bounds accesses. They will ensure that the +// underlying buffer is returned to the caller, or delete it if the current +// owner was responsible for allocating the request. In addition, they help +// provide an easy way to determine the size of the request by simply specifying +// the parent device's operation size. +// +// operation::OperationPool provides a simple pool that allows reuse of +// pre-allocated operation::Operation objects. +// +// operation::{Unowned,}OperationQueue provide safe queues to place operations +// in while they are pending. These queues rely on intrusive node data built +// into the wrapper type, stored in the private storage section. +// +// In order to use make use of the Operation and OperationNode classes, a new +// type must be created which inherits from it like so: +// +// class Foo : public Operation<Foo, OperationTraits, void>; +// class Bar : public UnownedOperation<Bar, OperationTraits, CallbackTraits, void>; +// +// OperationTraits must be a type which implements the following function +// and type signatures: +// +// // Defines public definition which is wrapped. +// using OperationType = foo_operation_t; +// +// // Performs an allocation for the operation. +// static OperationType* Alloc(size_t op_size); +// +// // Frees the allocation created by Alloc above. +// static void Free(OperationType* op); +// +// CallbackTraits must be a type which implements the following function +// and type signatures: +// +// // The type here can be anything. It should match the callback provided to +// // the UnownedOperation constructor. +// using CallbackType = void(void* ctx, ARGS, foo_operation_t*); +// +// // In case Complete is not called by UnownedOperation owners, these are +// // the args to trigger Complete with. +// static std::tuple<ARGS> AutoCompleteArgs(); +// +// // Should call the callback, transforming and positioning args as necessary. +// static void Callback(CallbackType*, void*, OperationType*, ARGS); +// +// Where ARGS is a variadic number of types left to the implementer. +// + +template <typename T, typename OperationTraits, typename CallbackTraits, typename Storage = void> +class OperationNode; + +template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage> +class OperationBase { +public: + using NodeType = OperationNode<D, OperationTraits, CallbackTraits, Storage>; + using OperationType = typename OperationTraits::OperationType; + + OperationType* take() __WARN_UNUSED_RESULT { + auto* tmp = operation_; + operation_ = nullptr; + return tmp; + } + + OperationType* operation() const { + return operation_; + } + + static constexpr size_t OperationSize(size_t parent_op_size) { + return fbl::round_up(parent_op_size, kAlignment) + sizeof(NodeType); + } + + size_t size() const { + return node_offset_ + sizeof(NodeType); + } + + // Returns private node stored inline + NodeType* node() { + auto* node = reinterpret_cast<NodeType*>( + reinterpret_cast<uintptr_t>(operation_) + node_offset_); + return node; + } + + Storage* private_storage() { + static_assert(!std::is_same<Storage, void>::value, + "private_storage not available on void type."); + return node()->private_storage(); + } + +protected: + OperationBase(OperationType* operation, size_t parent_op_size) + : operation_(operation), + node_offset_(fbl::round_up(parent_op_size, kAlignment)) { + ZX_DEBUG_ASSERT(operation != nullptr); + } + + OperationBase(OperationBase&& other) + : operation_(other.operation_), node_offset_(other.node_offset_) { + other.operation_ = nullptr; + } + + OperationBase& operator=(OperationBase&& other) { + operation_ = other.operation_; + node_offset_ = other.node_offset_; + other.operation_ = nullptr; + return *this; + } + + OperationBase(const OperationBase& other) = delete; + OperationBase& operator=(const OperationBase& other) = delete; + + OperationType* operation_; + zx_off_t node_offset_; + +private: + static constexpr size_t kAlignment = alignof(NodeType); +}; + +template <typename D, typename OperationTraits, typename Storage = void> +class Operation : public OperationBase<D, OperationTraits, void, Storage> { +public: + using BaseClass = OperationBase<D, OperationTraits, void, Storage>; + using NodeType = OperationNode<D, OperationTraits, void, Storage>; + using OperationType = typename OperationTraits::OperationType; + + // Creates a new operation with payload space of data_size. + static std::optional<D> Alloc(size_t op_size, + size_t parent_op_size = sizeof(OperationType)) { + OperationType* op = OperationTraits::Alloc(op_size); + if (op == nullptr) { + return std::nullopt; + } + + D out(op, parent_op_size); + new (out.node()) NodeType(out.node_offset_); + return std::move(out); + } + + // Must be called with |operation| allocated via OperationTraits::Alloc. + Operation(OperationType* operation, size_t parent_op_size) + : BaseClass(operation, parent_op_size) {} + + Operation(Operation&& other) + : BaseClass(other.operation_, other.node_offset_) { + other.operation_ = nullptr; + } + + Operation& operator=(Operation&& other) { + BaseClass::operator=(std::move(other)); + return *this; + } + + ~Operation() { + Release(); + } + + void Release() { + if (BaseClass::operation_) { + BaseClass::node()->NodeType::~NodeType(); + OperationTraits::Free(BaseClass::take()); + } + } +}; + +// Similar to operation::Operation, but it doesn't call free on destruction. +// This should be used to wrap NodeType* objects allocated in other +// drivers. +template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage = void> +class UnownedOperation : public OperationBase<D, OperationTraits, CallbackTraits, Storage> { +public: + using BaseClass = OperationBase<D, OperationTraits, CallbackTraits, Storage>; + using NodeType = OperationNode<D, OperationTraits, CallbackTraits, Storage>; + using OperationType = typename OperationTraits::OperationType; + using CallbackType = typename CallbackTraits::CallbackType; + + UnownedOperation(OperationType* operation, const CallbackType* complete_cb, void* cookie, + size_t parent_op_size) + : BaseClass(operation, parent_op_size) { + new (BaseClass::node()) NodeType(BaseClass::node_offset_, complete_cb, cookie); + } + + UnownedOperation(OperationType* operation, size_t parent_op_size) + : BaseClass(operation, parent_op_size) { + ZX_DEBUG_ASSERT(BaseClass::node()->node_offset() != 0); + } + + UnownedOperation(UnownedOperation&& other) + : BaseClass(other.operation_, other.node_offset_) { + other.operation_ = nullptr; + } + + UnownedOperation& operator=(UnownedOperation&& other) { + BaseClass::operator=(std::move(other)); + return *this; + } + + ~UnownedOperation() { + // Auto-complete if it wasn't. + auto complete = [this](auto... args) { + this->Complete(std::forward<decltype(args)>(args)...); + }; + std::apply(complete, CallbackTraits::AutoCompleteArgs()); + } + + // Must be called by the processor when the operation has completed or failed. + // The operation and any virtual or physical memory obtained from it is no + // longer valid after Complete is called. + template <typename... Args> + void Complete(Args... args) { + if (BaseClass::operation_) { + auto* complete_cb = BaseClass::node()->complete_cb(); + auto* cookie = BaseClass::node()->cookie(); + BaseClass::node()->NodeType::~NodeType(); + CallbackTraits::Callback(complete_cb, cookie, BaseClass::take(), + std::forward<Args>(args)...); + } + } +}; + +// Node storage for operation::Operation and operation::UnownedOperation. Does not maintain +// ownership of underlying NodeType*. Must be transformed back into +// appopriate wrapper type to maintain correct ownership. +// It is strongly recommended to use operation::OperationPool and operation::OperationQueue to +// avoid ownership pitfalls. +template <typename T, typename OperationTraits, typename CallbackTraits, typename Storage> +class OperationNode : public fbl::DoublyLinkedListable< + OperationNode<T, OperationTraits, CallbackTraits, Storage>*> { +public: + using OperationType = typename OperationTraits::OperationType; + using CallbackType = typename CallbackTraits::CallbackType; + + OperationNode(zx_off_t node_offset, const CallbackType* complete_cb, void* cookie) + : node_offset_(node_offset), complete_cb_(complete_cb), cookie_(cookie) {} + + ~OperationNode() = default; + + T operation() const { + return T( + reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_), + node_offset_); + } + + zx_off_t node_offset() const { return node_offset_; } + + const CallbackType* complete_cb() const { + return complete_cb_; + } + + void* cookie() const { + return cookie_; + } + + Storage* private_storage() { + return &private_storage_; + } + +private: + const zx_off_t node_offset_; + const CallbackType* complete_cb_; + void* cookie_; + Storage private_storage_; +}; + +// Specialized version for when complete_cb is not required. +template <typename T, typename OperationTraits, typename Storage> +class OperationNode<T, OperationTraits, void, Storage> : public fbl::DoublyLinkedListable< + OperationNode<T, OperationTraits, void, + Storage>*> { +public: + using OperationType = typename OperationTraits::OperationType; + + explicit OperationNode(zx_off_t node_offset) + : node_offset_(node_offset) {} + + ~OperationNode() = default; + + T operation() const { + return T( + reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_), + node_offset_); + } + + zx_off_t node_offset() const { return node_offset_; } + + Storage* private_storage() { + return &private_storage_; + } + +private: + const zx_off_t node_offset_; + Storage private_storage_; +}; + +// Specialized version for when no additional storage is required. +template <typename T, typename OperationTraits, typename CallbackTraits> +class OperationNode<T, OperationTraits, CallbackTraits, void> : public fbl::DoublyLinkedListable< + OperationNode<T, + OperationTraits, + CallbackTraits, + void>*> { +public: + using OperationType = typename OperationTraits::OperationType; + using CallbackType = typename CallbackTraits::CallbackType; + + OperationNode(zx_off_t node_offset, const CallbackType* complete_cb, void* cookie) + : node_offset_(node_offset), complete_cb_(complete_cb), cookie_(cookie) {} + + ~OperationNode() = default; + + T operation() const { + return T( + reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_), + node_offset_); + } + + zx_off_t node_offset() const { return node_offset_; } + + const CallbackType* complete_cb() const { + return complete_cb_; + } + + void* cookie() const { + return cookie_; + } + +private: + const zx_off_t node_offset_; + const CallbackType* complete_cb_; + void* cookie_; +}; + +// Specialized version for when no additional storage is required, and +// complete_cb is not required. +template <typename T, typename OperationTraits> +class OperationNode<T, OperationTraits, void, void> : public fbl::DoublyLinkedListable< + OperationNode<T, OperationTraits, void, + void>*> { +public: + using OperationType = typename OperationTraits::OperationType; + + OperationNode(zx_off_t node_offset) + : node_offset_(node_offset) {} + + ~OperationNode() = default; + + T operation() const { + return T( + reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_), + node_offset_); + } + + zx_off_t node_offset() const { return node_offset_; } + +private: + const zx_off_t node_offset_; +}; + +// Convenience queue wrapper around fbl::DoublyLinkedList<T>. +// The class is thread-safe. +template <typename OpType, typename OperationTraits, typename CallbackTraits, typename Storage> +class BaseQueue { +public: + using NodeType = OperationNode<OpType, OperationTraits, CallbackTraits, Storage>; + + DECLARE_HAS_MEMBER_FN_WITH_SIGNATURE(has_node, node, NodeType* (C::*)()); + static_assert(has_node<OpType>::value, + "OpType must implement OperationNode<OpType, OperationTraits, CallbackTraits, " + "Storage>* node()"); + + BaseQueue() {} + + ~BaseQueue() { + Release(); + } + + BaseQueue(BaseQueue&& other) { + fbl::AutoLock al(&other.lock_); + queue_.swap(other.queue_); + } + + BaseQueue& operator=(BaseQueue&& other) { + fbl::AutoLock al1(&lock_); + fbl::AutoLock al2(&other.lock_); + queue_.clear(); + queue_.swap(other.queue_); + return *this; + } + + void push(OpType op) { + fbl::AutoLock al(&lock_); + auto* node = op.node(); + queue_.push_front(node); + // Must prevent Complete/Release from being called in destructor. + __UNUSED auto dummy = op.take(); + } + + void push_next(OpType op) { + fbl::AutoLock al(&lock_); + auto* node = op.node(); + queue_.push_back(node); + // Must prevent Complete/Release from being called in destructor. + __UNUSED auto dummy = op.take(); + } + + std::optional<OpType> pop() { + fbl::AutoLock al(&lock_); + auto* node = queue_.pop_back(); + if (node) { + return std::move(node->operation()); + } + return std::nullopt; + } + + bool is_empty() { + fbl::AutoLock al(&lock_); + return queue_.is_empty(); + } + + // Releases all ops stored in the queue. + void Release() { + fbl::AutoLock al(&lock_); + while (!queue_.is_empty()) { + // Tranform back into operation to force correct destructor to run. + __UNUSED auto op = queue_.pop_back()->operation(); + } + } + +protected: + fbl::Mutex lock_; + fbl::DoublyLinkedList<NodeType*> queue_ __TA_GUARDED(lock_); +}; + +template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage = void> +using UnownedOperationQueue = BaseQueue<D, OperationTraits, CallbackTraits, Storage>; + +template <typename D, typename OperationTraits, typename Storage = void> +using OperationQueue = BaseQueue<D, OperationTraits, void, Storage>; + +// A driver may use operation::OperationPool for recycling their own operations. +template <typename OpType, typename OperationTraits, typename Storage = void> +class OperationPool : protected BaseQueue<OpType, OperationTraits, void, Storage> { +public: + using BaseClass = BaseQueue<OpType, OperationTraits, void, Storage>; + + // Use constructor. + using BaseClass::BaseClass; + + // Operate like a stack rather than a queue. + void push(OpType op) { + BaseClass::push_next(std::forward<OpType>(op)); + } + using BaseClass::pop; + + using BaseClass::Release; +}; + +} // namespace operation
diff --git a/system/dev/lib/operation/rules.mk b/system/dev/lib/operation/rules.mk new file mode 100644 index 0000000..7974173 --- /dev/null +++ b/system/dev/lib/operation/rules.mk
@@ -0,0 +1,18 @@ +# 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. + +LOCAL_DIR := $(GET_LOCAL_DIR) + +MODULE := $(LOCAL_DIR) + +MODULE_TYPE := userlib + +MODULE_SRCS := \ + $(LOCAL_DIR)/dummy.cpp \ + +MODULE_STATIC_LIBS := \ + system/ulib/fbl \ + system/ulib/zx \ + +include make/module.mk
diff --git a/system/dev/test/operation/main.cpp b/system/dev/test/operation/main.cpp new file mode 100644 index 0000000..8af51d4 --- /dev/null +++ b/system/dev/test/operation/main.cpp
@@ -0,0 +1,10 @@ +// 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. +#include <lib/operation/operation.h> + +#include <unittest/unittest.h> + +int main(int argc, char** argv) { + return unittest_run_all_tests(argc, argv) ? 0 : -1; +}
diff --git a/system/dev/test/operation/operation-pool-test.cpp b/system/dev/test/operation/operation-pool-test.cpp new file mode 100644 index 0000000..6ce1b66 --- /dev/null +++ b/system/dev/test/operation/operation-pool-test.cpp
@@ -0,0 +1,129 @@ +// 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. + +#include <lib/operation/operation.h> + +#include <unittest/unittest.h> + +namespace { + +struct TestOp { + int dummy; +}; + +struct TestOpTraits { + using OperationType = TestOp; + + static OperationType* Alloc(size_t op_size) { + fbl::AllocChecker ac; + fbl::unique_ptr<uint8_t[]> raw; + if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { + raw = fbl::unique_ptr<uint8_t[]>( + new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]); + } else { + raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]); + } + if (!ac.check()) { + return nullptr; + } + return reinterpret_cast<TestOp*>(raw.release()); + } + + static void Free(OperationType* op) { + delete[] reinterpret_cast<uint8_t*>(op); + } +}; + +using TestOpCallback = void (*)(void*, zx_status_t, TestOp*); + +struct CallbackTraits { + using CallbackType = TestOpCallback; + + static std::tuple<zx_status_t> AutoCompleteArgs() { + return std::make_tuple(ZX_ERR_INTERNAL); + } + + static void Callback(const CallbackType* callback, void* cookie, TestOp* op, + zx_status_t status) { + (*callback)(cookie, status, op); + } +}; + +struct Operation : public operation::Operation<Operation, TestOpTraits, void> { + using BaseClass = operation::Operation<Operation, TestOpTraits, void>; + using BaseClass::BaseClass; +}; + +struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void> { + + using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void>; + using BaseClass::BaseClass; +}; + +using OperationPool = operation::OperationPool<Operation, TestOpTraits, void>; + +constexpr size_t kParentOpSize = sizeof(TestOp); +constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize); + +bool TrivialLifetimeTest() { + BEGIN_TEST; + OperationPool pool; + END_TEST; +} + +bool SingleOperationTest() { + BEGIN_TEST; + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + + OperationPool pool; + EXPECT_TRUE(pool.pop() == std::nullopt); + pool.push(std::move(*operation)); + EXPECT_TRUE(pool.pop() != std::nullopt); + EXPECT_TRUE(pool.pop() == std::nullopt); + END_TEST; +} + +bool MultipleOperationTest() { + BEGIN_TEST; + OperationPool pool; + + for (size_t i = 0; i < 10; i++) { + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + pool.push(std::move(*operation)); + } + + for (size_t i = 0; i < 10; i++) { + EXPECT_TRUE(pool.pop() != std::nullopt); + } + EXPECT_TRUE(pool.pop() == std::nullopt); + END_TEST; +} + +bool ReleaseTest() { + BEGIN_TEST; + OperationPool pool; + + for (size_t i = 0; i < 10; i++) { + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + pool.push(std::move(*operation)); + } + + pool.Release(); + EXPECT_TRUE(pool.pop() == std::nullopt); + END_TEST; +} + +} // namespace + +BEGIN_TEST_CASE(OperationPoolTests) +RUN_TEST_SMALL(TrivialLifetimeTest) +RUN_TEST_SMALL(SingleOperationTest) +RUN_TEST_SMALL(MultipleOperationTest) +RUN_TEST_SMALL(ReleaseTest) +END_TEST_CASE(OperationPoolTests)
diff --git a/system/dev/test/operation/operation-queue-test.cpp b/system/dev/test/operation/operation-queue-test.cpp new file mode 100644 index 0000000..55b7c54 --- /dev/null +++ b/system/dev/test/operation/operation-queue-test.cpp
@@ -0,0 +1,278 @@ +// 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. + +#include <lib/operation/operation.h> + +#include <unittest/unittest.h> + +namespace { + +struct TestOp { + int dummy; +}; + +struct TestOpTraits { + using OperationType = TestOp; + + static OperationType* Alloc(size_t op_size) { + fbl::AllocChecker ac; + fbl::unique_ptr<uint8_t[]> raw; + if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { + raw = fbl::unique_ptr<uint8_t[]>( + new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]); + } else { + raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]); + } + if (!ac.check()) { + return nullptr; + } + return reinterpret_cast<TestOp*>(raw.release()); + } + + static void Free(OperationType* op) { + delete[] reinterpret_cast<uint8_t*>(op); + } +}; + +using TestOpCallback = void (*)(void*, zx_status_t, TestOp*); + +struct CallbackTraits { + using CallbackType = TestOpCallback; + + static std::tuple<zx_status_t> AutoCompleteArgs() { + return std::make_tuple(ZX_ERR_INTERNAL); + } + + static void Callback(const CallbackType* callback, void* cookie, TestOp* op, + zx_status_t status) { + (*callback)(cookie, status, op); + } +}; + +struct Operation : public operation::Operation<Operation, TestOpTraits, void> { + using BaseClass = operation::Operation<Operation, TestOpTraits, void>; + using BaseClass::BaseClass; +}; + +struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void> { + using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void>; + using BaseClass::BaseClass; +}; + +using OperationQueue = operation::OperationQueue<Operation, TestOpTraits, void>; +using UnownedOperationQueue = operation::UnownedOperationQueue<UnownedOperation, TestOpTraits, + CallbackTraits, void>; + +constexpr size_t kParentOpSize = sizeof(TestOp); +constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize); + +bool TrivialLifetimeTest() { + BEGIN_TEST; + OperationQueue queue; + UnownedOperationQueue unowned_queue; + END_TEST; +} + +bool SingleOperationTest() { + BEGIN_TEST; + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + + OperationQueue queue; + EXPECT_TRUE(queue.pop() == std::nullopt); + queue.push(std::move(*operation)); + EXPECT_TRUE(queue.pop() != std::nullopt); + EXPECT_TRUE(queue.pop() == std::nullopt); + END_TEST; +} + +bool MultipleOperationTest() { + BEGIN_TEST; + OperationQueue queue; + + for (size_t i = 0; i < 10; i++) { + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + queue.push(std::move(*operation)); + } + + for (size_t i = 0; i < 10; i++) { + EXPECT_TRUE(queue.pop() != std::nullopt); + } + EXPECT_TRUE(queue.pop() == std::nullopt); + END_TEST; +} + +bool ReleaseTest() { + BEGIN_TEST; + OperationQueue queue; + + for (size_t i = 0; i < 10; i++) { + std::optional<Operation> operation = Operation::Alloc(kOpSize); + ASSERT_TRUE(operation.has_value()); + queue.push(std::move(*operation)); + } + + queue.Release(); + EXPECT_TRUE(queue.pop() == std::nullopt); + END_TEST; +} + +bool MultipleLayerTest() { + BEGIN_TEST; + + using FirstLayerOp = UnownedOperation; + using SecondLayerOp = Operation; + + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize); + + OperationQueue queue; + for (size_t i = 0; i < 10; i++) { + std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize, + kFirstLayerOpSize); + ASSERT_TRUE(operation.has_value()); + queue.push(std::move(*operation)); + } + + UnownedOperationQueue queue2; + size_t count = 0; + for (auto operation = queue.pop(); operation; operation = queue.pop()) { + FirstLayerOp unowned(operation->take(), nullptr, nullptr, kBaseOpSize); + queue2.push(std::move(unowned)); + ++count; + } + EXPECT_EQ(count, 10); + + count = 0; + for (auto unowned = queue2.pop(); unowned; unowned = queue2.pop()) { + SecondLayerOp operation(unowned->take(), kFirstLayerOpSize); + queue.push(std::move(operation)); + ++count; + } + EXPECT_EQ(count, 10); + + END_TEST; +} + +bool MultipleLayerWithStorageTest() { + BEGIN_TEST; + + struct FirstLayerOp : public operation::UnownedOperation<FirstLayerOp, TestOpTraits, + CallbackTraits, char> { + + using BaseClass = operation::UnownedOperation<FirstLayerOp, TestOpTraits, + CallbackTraits, char>; + using BaseClass::BaseClass; + }; + + struct SecondLayerOp : public operation::Operation<SecondLayerOp, TestOpTraits, uint64_t> { + using BaseClass = operation::Operation<SecondLayerOp, TestOpTraits, uint64_t>; + using BaseClass::BaseClass; + }; + + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize); + + operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t> queue; + for (size_t i = 0; i < 10; i++) { + std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize, + kFirstLayerOpSize); + ASSERT_TRUE(operation.has_value()); + *operation->private_storage() = i; + EXPECT_EQ(*operation->private_storage(), i); + queue.push(std::move(*operation)); + } + + operation::UnownedOperationQueue<FirstLayerOp, TestOpTraits, CallbackTraits, char> queue2; + size_t count = 0; + for (auto operation = queue.pop(); operation; operation = queue.pop()) { + FirstLayerOp unowned(operation->take(), nullptr, nullptr, kBaseOpSize); + *unowned.private_storage() = static_cast<char>('a' + count); + queue2.push(std::move(unowned)); + ++count; + } + EXPECT_EQ(count, 10); + + count = 0; + for (auto unowned = queue2.pop(); unowned; unowned = queue2.pop()) { + EXPECT_EQ(*unowned->private_storage(), static_cast<char>('a' + count)); + SecondLayerOp operation(unowned->take(), kFirstLayerOpSize); + EXPECT_EQ(*operation.private_storage(), count); + queue.push(std::move(operation)); + ++count; + } + EXPECT_EQ(count, 10); + + END_TEST; +} + +bool MultipleLayerWithCallbackTest() { + BEGIN_TEST; + + struct FirstLayerOp : public operation::UnownedOperation<FirstLayerOp, TestOpTraits, + CallbackTraits, char> { + using BaseClass = operation::UnownedOperation<FirstLayerOp, TestOpTraits, + CallbackTraits, char>; + using BaseClass::BaseClass; + }; + + struct SecondLayerOp : public operation::Operation<SecondLayerOp, TestOpTraits, uint64_t> { + using BaseClass = operation::Operation<SecondLayerOp, TestOpTraits, uint64_t>; + using BaseClass::BaseClass; + }; + + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize); + + operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t> queue; + for (size_t i = 0; i < 10; i++) { + std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize, + kFirstLayerOpSize); + ASSERT_TRUE(operation.has_value()); + *operation->private_storage() = i; + EXPECT_EQ(*operation->private_storage(), i); + queue.push(std::move(*operation)); + } + + auto callback = [](void* ctx, zx_status_t status, TestOp* operation) { + auto* queue = static_cast<operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t>*>(ctx); + queue->push(SecondLayerOp(operation, kFirstLayerOpSize)); + }; + TestOpCallback cb = callback; + + { + operation::UnownedOperationQueue<FirstLayerOp, TestOpTraits, CallbackTraits, char> queue2; + for (auto operation = queue.pop(); operation; operation = queue.pop()) { + FirstLayerOp unowned(operation->take(), &cb, &queue, kBaseOpSize); + queue2.push(std::move(unowned)); + } + } + + size_t count = 0; + for (auto operation = queue.pop(); operation; operation = queue.pop()) { + EXPECT_EQ(*operation->private_storage(), count); + ++count; + } + EXPECT_EQ(count, 10); + + END_TEST; +} + +} // namespace + +BEGIN_TEST_CASE(OperationQueueTests) +RUN_TEST_SMALL(TrivialLifetimeTest) +RUN_TEST_SMALL(SingleOperationTest) +RUN_TEST_SMALL(MultipleOperationTest) +RUN_TEST_SMALL(ReleaseTest) +RUN_TEST_SMALL(MultipleLayerTest) +RUN_TEST_SMALL(MultipleLayerWithStorageTest) +RUN_TEST_SMALL(MultipleLayerWithCallbackTest) +END_TEST_CASE(OperationQueueTests);
diff --git a/system/dev/test/operation/operation-test.cpp b/system/dev/test/operation/operation-test.cpp new file mode 100644 index 0000000..07a822d --- /dev/null +++ b/system/dev/test/operation/operation-test.cpp
@@ -0,0 +1,172 @@ +// 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. + +#include <lib/operation/operation.h> + +#include <fbl/algorithm.h> +#include <fbl/auto_call.h> +#include <unittest/unittest.h> + +namespace { + +struct TestOp { + int dummy; +}; + +struct TestOpTraits { + using OperationType = TestOp; + + static OperationType* Alloc(size_t op_size) { + fbl::AllocChecker ac; + fbl::unique_ptr<uint8_t[]> raw; + if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { + raw = fbl::unique_ptr<uint8_t[]>( + new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]); + } else { + raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]); + } + if (!ac.check()) { + return nullptr; + } + return reinterpret_cast<TestOp*>(raw.release()); + } + + static void Free(OperationType* op) { + delete[] reinterpret_cast<uint8_t*>(op); + } +}; + +using TestOpCallback = void (*)(void*, zx_status_t, TestOp*); + +struct CallbackTraits { + using CallbackType = TestOpCallback; + + static std::tuple<zx_status_t> AutoCompleteArgs() { + return std::make_tuple(ZX_ERR_INTERNAL); + } + + static void Callback(const CallbackType* callback, void* cookie, TestOp* op, + zx_status_t status) { + (*callback)(cookie, status, op); + } +}; + +struct Operation : public operation::Operation<Operation, TestOpTraits, void> { + using BaseClass = operation::Operation<Operation, TestOpTraits, void>; + using BaseClass::BaseClass; +}; + +struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void> { + + using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits, + CallbackTraits, void>; + using BaseClass::BaseClass; +}; + +constexpr size_t kParentOpSize = sizeof(TestOp); +constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize); + +bool AllocTest() { + BEGIN_TEST; + std::optional<Operation> op = Operation::Alloc(kOpSize); + EXPECT_TRUE(op.has_value()); + END_TEST; +} + +bool PrivateStorageTest() { + BEGIN_TEST; + + struct Private : public operation::Operation<Private, TestOpTraits, uint32_t> { + using BaseClass = operation::Operation<Private, TestOpTraits, uint32_t>; + using BaseClass::BaseClass; + }; + + constexpr size_t kOperationSize = Private::OperationSize(kParentOpSize); + auto operation = Private::Alloc(kOperationSize); + ASSERT_TRUE(operation.has_value()); + *operation->private_storage() = 1001; + ASSERT_EQ(*operation->private_storage(), 1001); + END_TEST; +} + +bool MultipleSectionTest() { + BEGIN_TEST; + + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = + UnownedOperation::OperationSize(kFirstLayerOpSize); + constexpr size_t kThirdLayerOpSize = + UnownedOperation::OperationSize(kSecondLayerOpSize); + + std::optional<Operation> operation = Operation::Alloc(kThirdLayerOpSize); + ASSERT_TRUE(operation.has_value()); + + UnownedOperation operation2(operation->take(), nullptr, nullptr, kFirstLayerOpSize); + UnownedOperation operation3(operation2.take(), nullptr, nullptr, kSecondLayerOpSize); + operation = Operation(operation3.take(), kBaseOpSize); + + END_TEST; +} + +bool CallbackTest() { + BEGIN_TEST; + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = + UnownedOperation::OperationSize(kFirstLayerOpSize); + + bool called = false; + auto callback = [](void* ctx, zx_status_t st, TestOp* operation) -> void { + *static_cast<bool*>(ctx) = true; + // We take ownership. + Operation unused(operation, kBaseOpSize); + }; + TestOpCallback cb = callback; + std::optional<Operation> operation = Operation::Alloc(kSecondLayerOpSize); + ASSERT_TRUE(operation.has_value()); + + UnownedOperation operation2(operation->take(), &cb, &called, kFirstLayerOpSize); + operation2.Complete(ZX_OK); + EXPECT_TRUE(called); + + END_TEST; +} + +bool AutoCallbackTest() { + BEGIN_TEST; + constexpr size_t kBaseOpSize = sizeof(TestOp); + constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize); + constexpr size_t kSecondLayerOpSize = + UnownedOperation::OperationSize(kFirstLayerOpSize); + + bool called = false; + auto callback = [](void* ctx, zx_status_t st, TestOp* operation) { + *static_cast<bool*>(ctx) = true; + // We take ownership. + Operation unused(operation, kBaseOpSize); + }; + TestOpCallback cb = callback; + + std::optional<Operation> operation = Operation::Alloc(kSecondLayerOpSize); + ASSERT_TRUE(operation.has_value()); + + { + UnownedOperation operation2(operation->take(), &cb, &called, kFirstLayerOpSize); + } + EXPECT_TRUE(called); + + END_TEST; +} + +} // namespace + +BEGIN_TEST_CASE(OperationTests) +RUN_TEST_SMALL(AllocTest) +RUN_TEST_SMALL(PrivateStorageTest) +RUN_TEST_SMALL(MultipleSectionTest) +RUN_TEST_SMALL(CallbackTest) +RUN_TEST_SMALL(AutoCallbackTest) +END_TEST_CASE(OperationTests);
diff --git a/system/dev/test/operation/rules.mk b/system/dev/test/operation/rules.mk new file mode 100644 index 0000000..1fba94d --- /dev/null +++ b/system/dev/test/operation/rules.mk
@@ -0,0 +1,32 @@ +# 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. + +LOCAL_DIR := $(GET_LOCAL_DIR) + +MODULE := $(LOCAL_DIR).test + +MODULE_TYPE := usertest + +MODULE_NAME := operation-test + +MODULE_SRCS += \ + $(LOCAL_DIR)/operation-test.cpp \ + $(LOCAL_DIR)/operation-pool-test.cpp \ + $(LOCAL_DIR)/operation-queue-test.cpp \ + $(LOCAL_DIR)/main.cpp \ + +MODULE_STATIC_LIBS := \ + system/dev/lib/fake_ddk \ + system/dev/lib/operation \ + system/ulib/ddk \ + system/ulib/fbl \ + system/ulib/zxcpp \ + +MODULE_LIBS := \ + system/ulib/c \ + system/ulib/fdio \ + system/ulib/unittest \ + system/ulib/zircon \ + +include make/module.mk