| // 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 "integration-test.h" |
| |
| #include <fcntl.h> |
| #include <lib/async/cpp/executor.h> |
| #include <lib/async/cpp/wait.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <lib/fdio/unsafe.h> |
| #include <lib/fit/bridge.h> |
| #include <zircon/boot/image.h> |
| #include <zircon/status.h> |
| |
| namespace libdriver_integration_test { |
| |
| async::Loop IntegrationTest::loop_{&kAsyncLoopConfigNoAttachToCurrentThread}; |
| IntegrationTest::IsolatedDevmgr IntegrationTest::devmgr_; |
| |
| void IntegrationTest::SetUpTestCase() { DoSetup(false /* should_create_composite */); } |
| |
| void IntegrationTest::DoSetup(bool should_create_composite) { |
| // Set up the isolated devmgr instance for this test suite. Note that we |
| // only do this once for the whole suite, because it is currently an |
| // expensive process. Ideally we'd do this between every test. |
| auto args = IsolatedDevmgr::DefaultArgs(); |
| args.stdio = fbl::unique_fd(open("/dev/null", O_RDWR)); |
| args.load_drivers.push_back("/boot/driver/fragment.so"); |
| args.load_drivers.push_back("/boot/driver/fragment.proxy.so"); |
| args.driver_search_paths.push_back("/boot/driver"); |
| |
| // Rig up a get_boot_item that will send configuration information over to |
| // the sysdev driver. |
| args.get_boot_item = [should_create_composite](uint32_t type, uint32_t extra, zx::vmo* vmo, |
| uint32_t* length) { |
| vmo->reset(); |
| *length = 0; |
| if (type != ZBI_TYPE_DRV_BOARD_PRIVATE || extra != 0) { |
| return ZX_OK; |
| } |
| zx::vmo data; |
| zx_status_t status = zx::vmo::create(1, 0, &data); |
| if (status != ZX_OK) { |
| return status; |
| } |
| status = data.write(reinterpret_cast<const void*>(&should_create_composite), 0, |
| sizeof(should_create_composite)); |
| if (status != ZX_OK) { |
| return status; |
| } |
| *length = sizeof(should_create_composite); |
| *vmo = std::move(data); |
| return ZX_OK; |
| }; |
| |
| zx_status_t status = |
| IsolatedDevmgr::Create(std::move(args), loop_.dispatcher(), &IntegrationTest::devmgr_); |
| ASSERT_EQ(status, ZX_OK) << "failed to create IsolatedDevmgr"; |
| |
| IntegrationTest::devmgr_.SetExceptionCallback(DevmgrException); |
| } |
| |
| void IntegrationTest::TearDownTestCase() { IntegrationTest::devmgr_.reset(); } |
| |
| IntegrationTest::IntegrationTest() {} |
| |
| void IntegrationTest::SetUp() { |
| // We do this in SetUp() rather than the ctor, since gtest cannot assert in |
| // ctors. |
| |
| fdio_t* io = fdio_unsafe_fd_to_io(IntegrationTest::devmgr_.devfs_root().get()); |
| zx::channel chan(fdio_service_clone(fdio_unsafe_borrow_channel(io))); |
| zx_status_t status = devfs_.Bind(std::move(chan), IntegrationTest::loop_.dispatcher()); |
| fdio_unsafe_release(io); |
| ASSERT_EQ(status, ZX_OK) << "failed to connect to devfs"; |
| } |
| |
| IntegrationTest::~IntegrationTest() { |
| IntegrationTest::loop_.Quit(); |
| IntegrationTest::loop_.ResetQuit(); |
| } |
| |
| void IntegrationTest::DevmgrException() { |
| // Log an error in the currently running test |
| ADD_FAILURE() << "Crash inside devmgr job"; |
| IntegrationTest::loop_.Quit(); |
| } |
| |
| void IntegrationTest::RunPromise(Promise<void> promise) { |
| async::Executor executor(IntegrationTest::loop_.dispatcher()); |
| |
| auto new_promise = promise.then([&](Promise<void>::result_type& result) { |
| if (result.is_error()) { |
| ADD_FAILURE() << result.error(); |
| } |
| IntegrationTest::loop_.Quit(); |
| return result; |
| }); |
| |
| executor.schedule_task(std::move(new_promise)); |
| |
| zx_status_t status = IntegrationTest::loop_.Run(); |
| ASSERT_EQ(status, ZX_ERR_CANCELED); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::CreateFirstChild( |
| std::unique_ptr<RootMockDevice>* root_mock_device, std::unique_ptr<MockDevice>* child_device) { |
| return ExpectBind(root_mock_device, [root_mock_device, child_device](HookInvocation record, |
| Completer<void> completer) { |
| ActionList actions; |
| actions.AppendAddMockDevice(IntegrationTest::loop_.dispatcher(), (*root_mock_device)->path(), |
| "first_child", std::vector<zx_device_prop_t>{}, ZX_OK, |
| std::move(completer), child_device); |
| actions.AppendReturnStatus(ZX_OK); |
| return actions; |
| }); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectUnbindThenRelease( |
| const std::unique_ptr<MockDevice>& device) { |
| fit::bridge<void, Error> bridge; |
| auto unbind = ExpectUnbind(device, [unbind_reply_completer = std::move(bridge.completer)]( |
| HookInvocation record, Completer<void> completer) mutable { |
| completer.complete_ok(); |
| ActionList actions; |
| actions.AppendUnbindReply(std::move(unbind_reply_completer)); |
| return actions; |
| }); |
| auto reply_done = bridge.consumer.promise_or(::fit::error("unbind_reply_completer abandoned")); |
| return unbind.and_then(JoinPromises(std::move(reply_done), ExpectRelease(device))); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectBind( |
| std::unique_ptr<RootMockDevice>* root_mock_device, BindOnce::Callback actions_callback) { |
| fit::bridge<void, Error> bridge; |
| auto bind_hook = |
| std::make_unique<BindOnce>(std::move(bridge.completer), std::move(actions_callback)); |
| zx_status_t status = RootMockDevice::Create(devmgr_, IntegrationTest::loop_.dispatcher(), |
| std::move(bind_hook), root_mock_device); |
| PROMISE_ASSERT(ASSERT_EQ(status, ZX_OK)); |
| return bridge.consumer.promise_or(::fit::error("bind abandoned")); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectUnbind( |
| const std::unique_ptr<MockDevice>& device, UnbindOnce::Callback actions_callback) { |
| fit::bridge<void, Error> bridge; |
| auto unbind_hook = |
| std::make_unique<UnbindOnce>(std::move(bridge.completer), std::move(actions_callback)); |
| // Wrap the body in a promise, since we want to defer the evaluation of |
| // device->set_hooks. |
| return fit::make_promise([consumer = std::move(bridge.consumer), &device, |
| unbind_hook = std::move(unbind_hook)]() mutable { |
| device->set_hooks(std::move(unbind_hook)); |
| return consumer.promise_or(::fit::error("unbind abandoned")); |
| }); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectOpen( |
| const std::unique_ptr<MockDevice>& device, OpenOnce::Callback actions_callback) { |
| fit::bridge<void, Error> bridge; |
| auto open_hook = |
| std::make_unique<OpenOnce>(std::move(bridge.completer), std::move(actions_callback)); |
| // Wrap the body in a promise, since we want to defer the evaluation of |
| // device->set_hooks. |
| return fit::make_promise( |
| [consumer = std::move(bridge.consumer), &device, open_hook = std::move(open_hook)]() mutable { |
| device->set_hooks(std::move(open_hook)); |
| return consumer.promise_or(::fit::error("open abandoned")); |
| }); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectClose( |
| const std::unique_ptr<MockDevice>& device, CloseOnce::Callback actions_callback) { |
| fit::bridge<void, Error> bridge; |
| auto close_hook = |
| std::make_unique<CloseOnce>(std::move(bridge.completer), std::move(actions_callback)); |
| // Wrap the body in a promise, since we want to defer the evaluation of |
| // device->set_hooks. |
| return fit::make_promise([consumer = std::move(bridge.consumer), &device, |
| close_hook = std::move(close_hook)]() mutable { |
| device->set_hooks(std::move(close_hook)); |
| return consumer.promise_or(::fit::error("close abandoned")); |
| }); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::ExpectRelease( |
| const std::unique_ptr<MockDevice>& device) { |
| // Wrap the body in a promise, since we want to defer the evaluation of |
| // device->set_hooks. |
| return fit::make_promise([&device]() { |
| fit::bridge<void, Error> bridge; |
| ReleaseOnce::Callback func = [](HookInvocation record, Completer<void> completer) { |
| completer.complete_ok(); |
| }; |
| auto release_hook = std::make_unique<ReleaseOnce>(std::move(bridge.completer), std::move(func)); |
| device->set_hooks(std::move(release_hook)); |
| return bridge.consumer.promise_or(::fit::error("release abandoned")); |
| }); |
| } |
| |
| IntegrationTest::Promise<void> IntegrationTest::DoOpen( |
| const std::string& path, fidl::InterfacePtr<fuchsia::io::Node>* client, uint32_t flags) { |
| fidl::InterfaceRequest<fuchsia::io::Node> server( |
| client->NewRequest(IntegrationTest::IntegrationTest::loop_.dispatcher())); |
| PROMISE_ASSERT(ASSERT_TRUE(server.is_valid())); |
| |
| PROMISE_ASSERT(ASSERT_EQ(client->events().OnOpen, nullptr)); |
| fit::bridge<void, Error> bridge; |
| client->events().OnOpen = [client, completer = std::move(bridge.completer)]( |
| zx_status_t status, |
| std::unique_ptr<fuchsia::io::NodeInfo> info) mutable { |
| if (status != ZX_OK) { |
| std::string error("failed to open node: "); |
| error.append(zx_status_get_string(status)); |
| completer.complete_error(std::move(error)); |
| client->events().OnOpen = nullptr; |
| return; |
| } |
| completer.complete_ok(); |
| client->events().OnOpen = nullptr; |
| }; |
| devfs_->Open(flags | fuchsia::io::OPEN_FLAG_DESCRIBE, 0, path, std::move(server)); |
| return bridge.consumer.promise_or(::fit::error("devfs open abandoned")); |
| } |
| |
| namespace { |
| |
| class AsyncWatcher { |
| public: |
| AsyncWatcher(std::string path, zx::channel watcher, fidl::InterfacePtr<fuchsia::io::Node> node) |
| : path_(std::move(path)), |
| watcher_(std::move(watcher)), |
| connections_{std::move(node), {}}, |
| wait_(watcher_.get(), ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0, |
| fit::bind_member(this, &AsyncWatcher::WatcherChanged)) {} |
| |
| void WatcherChanged(async_dispatcher_t* dispatcher, async::Wait* wait, zx_status_t status, |
| const zx_packet_signal_t* signal); |
| |
| zx_status_t Begin(async_dispatcher_t* dispatcher, |
| fit::completer<void, IntegrationTest::Error> completer) { |
| completer_ = std::move(completer); |
| return wait_.Begin(dispatcher); |
| } |
| |
| // Directory handle to keep alive for the lifetime of the AsyncWatcher, if |
| // necessary. |
| struct Connections { |
| fidl::InterfacePtr<fuchsia::io::Node> node; |
| fidl::InterfacePtr<fuchsia::io::Directory> directory; |
| }; |
| |
| Connections& connections() { return connections_; } |
| |
| private: |
| std::string path_; |
| zx::channel watcher_; |
| Connections connections_; |
| |
| async::Wait wait_; |
| fit::completer<void, IntegrationTest::Error> completer_; |
| }; |
| |
| void AsyncWatcher::WatcherChanged(async_dispatcher_t* dispatcher, async::Wait* wait, |
| zx_status_t status, const zx_packet_signal_t* signal) { |
| auto error = [&](const char* msg) { |
| completer_.complete_error(msg); |
| delete this; |
| }; |
| if (status != ZX_OK) { |
| return error("watcher error"); |
| } |
| if (signal->observed & ZX_CHANNEL_READABLE) { |
| char buf[fuchsia::io::MAX_BUF + 1]; |
| uint32_t bytes_read; |
| status = watcher_.read(0, buf, nullptr, sizeof(buf) - 1, 0, &bytes_read, nullptr); |
| if (status != ZX_OK) { |
| return error("watcher read error"); |
| } |
| |
| size_t bytes_processed = 0; |
| while (bytes_processed + 2 < bytes_read) { |
| char* msg = &buf[bytes_processed]; |
| uint8_t name_length = msg[1]; |
| |
| if (bytes_processed + 2 + name_length > bytes_read) { |
| return error("watcher read error"); |
| } |
| |
| char* filename = &msg[2]; |
| uint8_t tmp = filename[name_length]; |
| filename[name_length] = 0; |
| if (!strcmp(path_.c_str(), filename)) { |
| completer_.complete_ok(); |
| delete this; |
| return; |
| } |
| filename[name_length] = tmp; |
| bytes_processed += 2 + name_length; |
| } |
| |
| wait->Begin(dispatcher); |
| } else if (signal->observed & ZX_CHANNEL_PEER_CLOSED) { |
| return error("watcher closed"); |
| } |
| } |
| |
| void WaitForPath(const fidl::InterfacePtr<fuchsia::io::Directory>& dir, |
| async_dispatcher_t* dispatcher, std::string path, |
| fit::completer<void, IntegrationTest::Error> completer) { |
| zx::channel watcher, remote; |
| ASSERT_EQ(zx::channel::create(0, &watcher, &remote), ZX_OK); |
| |
| fidl::InterfacePtr<fuchsia::io::Node> last_dir; |
| std::string filename; |
| |
| size_t last_slash = path.find_last_of('/'); |
| if (last_slash != std::string::npos) { |
| std::string prefix(path, 0, last_slash); |
| |
| dir->Open(fuchsia::io::OPEN_FLAG_DIRECTORY | fuchsia::io::OPEN_FLAG_DESCRIBE | |
| fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE, |
| 0, prefix, last_dir.NewRequest(dispatcher)); |
| filename = path.substr(last_slash + 1); |
| } else { |
| dir->Clone(fuchsia::io::CLONE_FLAG_SAME_RIGHTS | fuchsia::io::OPEN_FLAG_DESCRIBE, |
| last_dir.NewRequest(dispatcher)); |
| filename = path; |
| } |
| |
| auto async_watcher = |
| std::make_unique<AsyncWatcher>(std::move(filename), std::move(watcher), std::move(last_dir)); |
| auto& events = async_watcher->connections().node.events(); |
| events.OnOpen = [dispatcher, async_watcher = std::move(async_watcher), |
| completer = std::move(completer), remote = std::move(remote)]( |
| zx_status_t status, std::unique_ptr<fuchsia::io::NodeInfo> info) mutable { |
| if (status != ZX_OK) { |
| completer.complete_error("Failed to open directory"); |
| return; |
| } |
| |
| auto& dir = async_watcher->connections().directory; |
| dir.Bind(async_watcher->connections().node.Unbind().TakeChannel(), dispatcher); |
| |
| dir->Watch(fuchsia::io::WATCH_MASK_ADDED | fuchsia::io::WATCH_MASK_EXISTING, 0, |
| std::move(remote), |
| [dispatcher, async_watcher = std::move(async_watcher), |
| completer = std::move(completer)](zx_status_t status) mutable { |
| if (status == ZX_OK) { |
| status = async_watcher->Begin(dispatcher, std::move(completer)); |
| if (status == ZX_OK) { |
| // The async_watcher will clean this up |
| __UNUSED auto ptr = async_watcher.release(); |
| return; |
| } |
| } |
| completer.complete_error("watcher failed"); |
| }); |
| }; |
| } |
| |
| } // namespace |
| |
| IntegrationTest::Promise<void> IntegrationTest::DoWaitForPath(const std::string& path) { |
| fit::bridge<void, Error> bridge; |
| WaitForPath(devfs_, IntegrationTest::loop_.dispatcher(), path, std::move(bridge.completer)); |
| return bridge.consumer.promise_or(::fit::error("WaitForPath abandoned")); |
| } |
| |
| } // namespace libdriver_integration_test |