[hid][buttons] Add unit tests

Also add DeviceGetMetadataSize to the fake_ddk and refactor/simplify.

ZX-3814 #done

Test: hid-buttons-test unit tests and trace buttons pressing on Astro
via the hid uapp as in:
$ hid read /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 3
hid read /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 3
hid: input thread started for
/dev/sys/platform/00:00:13/hid-buttons/hid-device-000
hid: /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 proto=0
hid: /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 num reports:
1
hid: /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 report ids...
ID 0x01 : TYPE   Input : SIZE 3 bytes
hid: /dev/sys/platform/00:00:13/hid-buttons/hid-device-000 maxreport=3
read returned 3
hid: input from /dev/sys/platform/00:00:13/hid-buttons/hid-device-000
01 00 00
read returned 3
hid: input from /dev/sys/platform/00:00:13/hid-buttons/hid-device-000
01 00 01
read returned 3
hid: input from /dev/sys/platform/00:00:13/hid-buttons/hid-device-000
01 01 01
hid: closing /dev/sys/platform/00:00:13/hid-buttons/hid-device-000
$

Change-Id: I5ca9cc8decd222f2fe872ce291bc6b8e7ba0833e
diff --git a/zircon/system/dev/input/hid-buttons/BUILD.gn b/zircon/system/dev/input/hid-buttons/BUILD.gn
index b99d892..6759b38 100644
--- a/zircon/system/dev/input/hid-buttons/BUILD.gn
+++ b/zircon/system/dev/input/hid-buttons/BUILD.gn
@@ -21,3 +21,27 @@
     "$zx/system/ulib/zx",
   ]
 }
+
+test("hid-buttons-test") {
+  output_name = "hid-buttons-test"
+  sources = [
+    "hid-buttons-test.cpp",
+    "hid-buttons.cpp",
+  ]
+  deps = [
+    "$zx/system/banjo/ddk.protocol.hidbus",
+    "$zx/system/banjo/ddk.protocol.platform.bus",
+    "$zx/system/banjo/ddk.protocol.platform.device",
+    "$zx/system/dev/lib/fake_ddk",
+    "$zx/system/dev/lib/mock-gpio",
+    "$zx/system/ulib/ddk",
+    "$zx/system/ulib/ddktl",
+    "$zx/system/ulib/fbl",
+    "$zx/system/ulib/hid",
+    "$zx/system/ulib/mock-function",
+    "$zx/system/ulib/sync",
+    "$zx/system/ulib/zircon",
+    "$zx/system/ulib/zx",
+    "$zx/system/ulib/zxtest",
+  ]
+}
diff --git a/zircon/system/dev/input/hid-buttons/hid-buttons-test.cpp b/zircon/system/dev/input/hid-buttons/hid-buttons-test.cpp
new file mode 100644
index 0000000..9cfbf6b
--- /dev/null
+++ b/zircon/system/dev/input/hid-buttons/hid-buttons-test.cpp
@@ -0,0 +1,432 @@
+// 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 "hid-buttons.h"
+
+#include <ddk/metadata.h>
+#include <ddk/metadata/buttons.h>
+#include <ddk/protocol/platform/bus.h>
+#include <ddktl/protocol/gpio.h>
+#include <lib/fake_ddk/fake_ddk.h>
+#include <lib/mock-gpio/mock-gpio.h>
+#include <zxtest/zxtest.h>
+
+namespace {
+static const buttons_button_config_t buttons_direct[] = {
+    {BUTTONS_TYPE_DIRECT, BUTTONS_ID_VOLUME_UP, 0, 0, 0},
+};
+
+static const buttons_gpio_config_t gpios_direct[] = {
+    {BUTTONS_GPIO_TYPE_INTERRUPT, 0, {GPIO_NO_PULL}}};
+
+static const buttons_button_config_t buttons_matrix[] = {
+    {BUTTONS_TYPE_MATRIX, BUTTONS_ID_VOLUME_UP, 0, 2, 0},
+    {BUTTONS_TYPE_MATRIX, BUTTONS_ID_KEY_A, 1, 2, 0},
+    {BUTTONS_TYPE_MATRIX, BUTTONS_ID_KEY_M, 0, 3, 0},
+    {BUTTONS_TYPE_MATRIX, BUTTONS_ID_PLAY_PAUSE, 1, 3, 0},
+};
+
+static const buttons_gpio_config_t gpios_matrix[] = {
+    {BUTTONS_GPIO_TYPE_INTERRUPT, 0, {GPIO_PULL_UP}},
+    {BUTTONS_GPIO_TYPE_INTERRUPT, 0, {GPIO_PULL_UP}},
+    {BUTTONS_GPIO_TYPE_MATRIX_OUTPUT, 0, {0}},
+    {BUTTONS_GPIO_TYPE_MATRIX_OUTPUT, 0, {0}},
+};
+} // namespace
+
+namespace buttons {
+
+enum class TestType {
+    kTestDirect,
+    kTestMatrix,
+};
+
+class FakeDdkButtons : public fake_ddk::Bind {
+public:
+    FakeDdkButtons(TestType type)
+        : Bind(), type_(type) {}
+
+    zx_status_t DeviceGetMetadata(zx_device_t* dev, uint32_t type, void* buf, size_t buflen,
+                                  size_t* actual) {
+        switch (type) {
+        case DEVICE_METADATA_BUTTONS_BUTTONS:
+            switch (type_) {
+            case TestType::kTestDirect:
+                *actual = sizeof(buttons_direct);
+                if (buflen != sizeof(buttons_direct)) {
+                    return ZX_ERR_INTERNAL;
+                }
+                memcpy(buf, &buttons_direct[0], *actual);
+                break;
+            case TestType::kTestMatrix:
+                *actual = sizeof(buttons_matrix);
+                if (buflen != sizeof(buttons_matrix)) {
+                    return ZX_ERR_INTERNAL;
+                }
+                memcpy(buf, &buttons_matrix[0], *actual);
+                break;
+            }
+            break;
+        case DEVICE_METADATA_BUTTONS_GPIOS:
+            switch (type_) {
+            case TestType::kTestDirect:
+                *actual = sizeof(gpios_direct);
+                if (buflen != sizeof(gpios_direct)) {
+                    return ZX_ERR_INTERNAL;
+                }
+                memcpy(buf, &gpios_direct[0], *actual);
+                break;
+            case TestType::kTestMatrix:
+                *actual = sizeof(gpios_matrix);
+                if (buflen != sizeof(gpios_matrix)) {
+                    return ZX_ERR_INTERNAL;
+                }
+                memcpy(buf, &gpios_matrix[0], *actual);
+                break;
+            }
+            break;
+        default:
+            return ZX_ERR_INTERNAL;
+        }
+        return ZX_OK;
+    }
+
+    zx_status_t DeviceGetMetadataSize(zx_device_t* dev, uint32_t type, size_t* out_size) {
+        switch (type) {
+        case DEVICE_METADATA_BUTTONS_BUTTONS:
+            switch (type_) {
+            case TestType::kTestDirect:
+                *out_size = sizeof(buttons_direct);
+                break;
+            case TestType::kTestMatrix:
+                *out_size = sizeof(buttons_matrix);
+                break;
+            }
+            break;
+        case DEVICE_METADATA_BUTTONS_GPIOS:
+            switch (type_) {
+            case TestType::kTestDirect:
+                *out_size = sizeof(gpios_direct);
+                break;
+            case TestType::kTestMatrix:
+                *out_size = sizeof(gpios_matrix);
+                break;
+            }
+            break;
+        default:
+            return ZX_ERR_INTERNAL;
+        }
+        return ZX_OK;
+    }
+
+private:
+    TestType type_;
+};
+
+class HidButtonsDeviceTest : public HidButtonsDevice {
+public:
+    HidButtonsDeviceTest(mock_gpio::MockGpio* gpios, size_t gpios_count, TestType type)
+        : HidButtonsDevice(fake_ddk::kFakeParent), type_(type) {
+        fbl::AllocChecker ac;
+        using MockPointer = mock_gpio::MockGpio*;
+        gpio_mocks_.reset(new (&ac) MockPointer[gpios_count], gpios_count);
+        for (size_t i = 0; i < gpios_count; ++i) {
+            gpio_mocks_[i] = &gpios[i];
+        }
+    }
+    void ShutDownTest() {
+        HidButtonsDevice::ShutDown();
+    }
+
+    void SetupGpio(const zx::interrupt& irq, size_t gpio_index) {
+        auto* mock = gpio_mocks_[gpio_index];
+        mock->ExpectSetAltFunction(ZX_OK, 0);
+        switch (type_) {
+        case TestType::kTestDirect:
+            mock->ExpectConfigIn(ZX_OK, GPIO_NO_PULL)
+                .ExpectRead(ZX_OK, 0) // Not pushed, low.
+                .ExpectReleaseInterrupt(ZX_OK)
+                .ExpectGetInterrupt(ZX_OK, ZX_INTERRUPT_MODE_EDGE_HIGH, irq);
+
+            // Make sure polarity is correct in case it changed during configuration.
+            mock->ExpectRead(ZX_OK, 0)                        // Not pushed.
+                .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Set correct polarity.
+                .ExpectRead(ZX_OK, 0);                        // Still not pushed.
+            break;
+        case TestType::kTestMatrix:
+            if (gpios_matrix[gpio_index].type == BUTTONS_GPIO_TYPE_INTERRUPT) {
+                mock->ExpectConfigIn(ZX_OK, gpios_matrix[gpio_index].internal_pull)
+                    .ExpectRead(ZX_OK, 0) // Not pushed, low.
+                    .ExpectReleaseInterrupt(ZX_OK)
+                    .ExpectGetInterrupt(ZX_OK, ZX_INTERRUPT_MODE_EDGE_HIGH, irq);
+
+                // Make sure polarity is correct in case it changed during configuration.
+                mock->ExpectRead(ZX_OK, 0)                        // Not pushed.
+                    .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Set correct polarity.
+                    .ExpectRead(ZX_OK, 0);                        // Still not pushed.
+            } else {
+                mock->ExpectConfigOut(ZX_OK, gpios_matrix[gpio_index].output_value);
+            }
+            break;
+        }
+    }
+
+    zx_status_t BindTest() {
+        FakeDdkButtons ddk(type_); // In these tests this is only used during binding.
+        fbl::Array<fake_ddk::ProtocolEntry> protocols(new fake_ddk::ProtocolEntry[1], 1);
+        protocols[0] = {ZX_PROTOCOL_PDEV, {nullptr, nullptr}};
+        ddk.SetProtocols(std::move(protocols));
+        return HidButtonsDevice::Bind();
+    }
+
+    zx_status_t PdevGetGpioProtocol(const pdev_protocol_t* proto, uint32_t index,
+                                    void* out_protocol_buffer, size_t out_protocol_size,
+                                    size_t* out_protocol_actual) override {
+        if (index > gpio_mocks_.size()) {
+            return ZX_ERR_INVALID_ARGS;
+        }
+        if (out_protocol_size != sizeof(gpio_protocol_t)) {
+            return ZX_ERR_INTERNAL;
+        }
+        gpio_protocol_t* p = reinterpret_cast<gpio_protocol_t*>(out_protocol_buffer);
+        *p = *gpio_mocks_[index]->GetProto();
+        *out_protocol_actual = sizeof(gpio_protocol_t);
+        return ZX_OK;
+    }
+
+    void FakeInterrupt() {
+        // Issue the first interrupt.
+        zx_port_packet packet = {kPortKeyInterruptStart + 0, ZX_PKT_TYPE_USER, ZX_OK, {}};
+        zx_status_t status = port_.queue(&packet);
+        ZX_ASSERT(status == ZX_OK);
+    }
+
+private:
+    TestType type_;
+    fbl::Array<mock_gpio::MockGpio*> gpio_mocks_;
+};
+
+TEST(HidButtonsTest, DirectButtonBind) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, DirectButtonPush) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)               // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                       // Still pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 1);              // Read value to prepare report.
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, DirectButtonUnpushedReport) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 0)                // Not Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Keep the correct polarity.
+        .ExpectRead(ZX_OK, 0);                        // Still not pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 0);               // Read value to prepare report.
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    hidbus_ifc_protocol_ops_t ops = {};
+    ops.io_queue = [](void* ctx, const void* buffer, size_t size) {
+        buttons_input_rpt_t report_volume_up = {};
+        report_volume_up.rpt_id = 1;
+        report_volume_up.volume_up = 0; // Unpushed.
+        ASSERT_BYTES_EQ(buffer, &report_volume_up, size);
+        EXPECT_EQ(size, sizeof(report_volume_up));
+    };
+    hidbus_ifc_protocol_t protocol = {};
+    protocol.ops = &ops;
+    device.HidbusStart(&protocol);
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, DirectButtonPushedReport) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)               // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                       // Still pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 1);              // Read value to prepare report.
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    hidbus_ifc_protocol_ops_t ops = {};
+    ops.io_queue = [](void* ctx, const void* buffer, size_t size) {
+        buttons_input_rpt_t report_volume_up = {};
+        report_volume_up.rpt_id = 1;
+        report_volume_up.volume_up = 1; // Pushed
+        ASSERT_BYTES_EQ(buffer, &report_volume_up, size);
+        EXPECT_EQ(size, sizeof(report_volume_up));
+    };
+    hidbus_ifc_protocol_t protocol = {};
+    protocol.ops = &ops;
+    device.HidbusStart(&protocol);
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, DirectButtonPushUnpushPush) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)               // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                       // Still pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 1);              // Read value to prepare report.
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 0)                // Not pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Turn the polarity.
+        .ExpectRead(ZX_OK, 0);                        // Still not pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 0);               // Read value to prepare report.
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)               // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                       // Still pushed, ok to continue.
+    mock_gpios[0].ExpectRead(ZX_OK, 1);              // Read value to prepare report.
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    device.FakeInterrupt();
+    device.FakeInterrupt();
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, DirectButtonFlaky) {
+    mock_gpio::MockGpio mock_gpios[1];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestDirect);
+    zx::interrupt irq;
+    zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irq);
+    device.SetupGpio(irq, 0);
+
+    // Reconfigure Polarity due to interrupt and keep checking until correct.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)                // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW)  // Turn the polarity.
+        .ExpectRead(ZX_OK, 0)                         // Oops now not pushed! not ok, retry.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1)                         // Oops pushed! not ok, retry.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW)  // Turn the polarity.
+        .ExpectRead(ZX_OK, 0)                         // Oops now not pushed! not ok, retry.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_HIGH) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1)                         // Oops pushed again! not ok, retry.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW)  // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                        // Now pushed and polarity set low, ok.
+    // Read value to generate report.
+    mock_gpios[0].ExpectRead(ZX_OK, 1); // Pushed.
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    ASSERT_NO_FATAL_FAILURES(mock_gpios[0].VerifyAndClear());
+}
+
+TEST(HidButtonsTest, MatrixButtonBind) {
+    mock_gpio::MockGpio mock_gpios[countof(gpios_matrix)];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestMatrix);
+    zx::interrupt irqs[countof(gpios_matrix)];
+    for (size_t i = 0; i < countof(gpios_matrix); ++i) {
+        zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irqs[i]);
+        device.SetupGpio(irqs[i], i);
+    }
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+    device.ShutDownTest();
+    for (size_t i = 0; i < countof(gpios_matrix); ++i) {
+        ASSERT_NO_FATAL_FAILURES(mock_gpios[i].VerifyAndClear());
+    }
+}
+
+TEST(HidButtonsTest, MatrixButtonPush) {
+    mock_gpio::MockGpio mock_gpios[countof(gpios_matrix)];
+    HidButtonsDeviceTest device(mock_gpios, countof(mock_gpios), TestType::kTestMatrix);
+    zx::interrupt irqs[countof(gpios_matrix)];
+    for (size_t i = 0; i < countof(gpios_matrix); ++i) {
+        zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &irqs[i]);
+        device.SetupGpio(irqs[i], i);
+    }
+
+    EXPECT_EQ(ZX_OK, device.BindTest());
+
+    // Reconfigure Polarity due to interrupt.
+    mock_gpios[0].ExpectRead(ZX_OK, 1)               // Pushed.
+        .ExpectSetPolarity(ZX_OK, GPIO_POLARITY_LOW) // Turn the polarity.
+        .ExpectRead(ZX_OK, 1);                       // Still pushed, ok to continue.
+
+    // Matrix Scan for 0.
+    mock_gpios[2].ExpectConfigIn(ZX_OK, GPIO_NO_PULL);                  // Float column.
+    mock_gpios[0].ExpectRead(ZX_OK, 1);                                 // Read row.
+    mock_gpios[2].ExpectConfigOut(ZX_OK, gpios_matrix[2].output_value); // Restore column.
+
+    // Matrix Scan for 1.
+    mock_gpios[2].ExpectConfigIn(ZX_OK, GPIO_NO_PULL);                  // Float column.
+    mock_gpios[1].ExpectRead(ZX_OK, 0);                                 // Read row.
+    mock_gpios[2].ExpectConfigOut(ZX_OK, gpios_matrix[2].output_value); // Restore column.
+
+    // Matrix Scan for 2.
+    mock_gpios[3].ExpectConfigIn(ZX_OK, GPIO_NO_PULL);                  // Float column.
+    mock_gpios[0].ExpectRead(ZX_OK, 0);                                 // Read row.
+    mock_gpios[3].ExpectConfigOut(ZX_OK, gpios_matrix[3].output_value); // Restore column.
+
+    // Matrix Scan for 3.
+    mock_gpios[3].ExpectConfigIn(ZX_OK, GPIO_NO_PULL);                  // Float column.
+    mock_gpios[1].ExpectRead(ZX_OK, 0);                                 // Read row.
+    mock_gpios[3].ExpectConfigOut(ZX_OK, gpios_matrix[3].output_value); // Restore colument.
+
+    hidbus_ifc_protocol_ops_t ops = {};
+    ops.io_queue = [](void* ctx, const void* buffer, size_t size) {
+        buttons_input_rpt_t report_volume_up = {};
+        report_volume_up.rpt_id = 1;
+        report_volume_up.volume_up = 1;
+        ASSERT_BYTES_EQ(buffer, &report_volume_up, size);
+        EXPECT_EQ(size, sizeof(report_volume_up));
+    };
+    hidbus_ifc_protocol_t protocol = {};
+    protocol.ops = &ops;
+    device.HidbusStart(&protocol);
+    device.FakeInterrupt();
+    device.ShutDownTest();
+    for (size_t i = 0; i < countof(gpios_matrix); ++i) {
+        ASSERT_NO_FATAL_FAILURES(mock_gpios[i].VerifyAndClear());
+    }
+}
+
+} // namespace buttons
diff --git a/zircon/system/dev/input/hid-buttons/hid-buttons.cpp b/zircon/system/dev/input/hid-buttons/hid-buttons.cpp
index c239946..6fc4849 100644
--- a/zircon/system/dev/input/hid-buttons/hid-buttons.cpp
+++ b/zircon/system/dev/input/hid-buttons/hid-buttons.cpp
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "hid-buttons.h"
+
 #include <string.h>
 #include <threads.h>
 #include <unistd.h>
@@ -11,26 +13,14 @@
 #include <ddk/platform-defs.h>
 #include <ddk/protocol/platform/bus.h>
 #include <ddk/protocol/platform/device.h>
-
-#include <hid/descriptor.h>
-
 #include <fbl/algorithm.h>
 #include <fbl/auto_call.h>
 #include <fbl/auto_lock.h>
 #include <fbl/unique_ptr.h>
-
+#include <hid/descriptor.h>
 #include <zircon/syscalls.h>
 #include <zircon/syscalls/port.h>
 
-#include "hid-buttons.h"
-
-// clang-format off
-// zx_port_packet::key.
-constexpr uint64_t PORT_KEY_SHUTDOWN = 0x01;
-// Start of up to kNumberOfRequiredGpios port types used for interrupts.
-constexpr uint64_t PORT_KEY_INTERRUPT_START = 0x10;
-// clang-format on
-
 namespace buttons {
 
 int HidButtonsDevice::Thread() {
@@ -43,12 +33,12 @@
             return thrd_error;
         }
 
-        if (packet.key == PORT_KEY_SHUTDOWN) {
+        if (packet.key == kPortKeyShutDown) {
             zxlogf(INFO, "%s shutting down\n", __FUNCTION__);
             return thrd_success;
-        } else if (packet.key >= PORT_KEY_INTERRUPT_START &&
-                   packet.key < (PORT_KEY_INTERRUPT_START + buttons_.size())) {
-            uint32_t type = static_cast<uint32_t>(packet.key - PORT_KEY_INTERRUPT_START);
+        } else if (packet.key >= kPortKeyInterruptStart &&
+                   packet.key < (kPortKeyInterruptStart + buttons_.size())) {
+            uint32_t type = static_cast<uint32_t>(packet.key - kPortKeyInterruptStart);
             if (gpios_[type].config.type == BUTTONS_GPIO_TYPE_INTERRUPT) {
                 // We need to reconfigure the GPIO to catch the opposite polarity.
                 ReconfigurePolarity(type, packet.key);
@@ -150,17 +140,6 @@
     buttons_input_rpt_t input_rpt = {};
     input_rpt.rpt_id = BUTTONS_RPT_ID_INPUT;
 
-    // Disable interrupts.
-    for (uint32_t i = 0; i < gpios_.size(); ++i) {
-        if (gpios_[i].config.type == BUTTONS_GPIO_TYPE_INTERRUPT) {
-            zx_status_t status = gpio_release_interrupt(&gpios_[i].gpio);
-            if (status != ZX_OK) {
-                zxlogf(ERROR, "%s gpio_release_interrupt failed %d\n", __FUNCTION__, status);
-                return status;
-            }
-        }
-    }
-
     for (size_t i = 0; i < buttons_.size(); ++i) {
         bool new_value = false; // A value true means a button is pressed.
         if (buttons_[i].type == BUTTONS_TYPE_MATRIX) {
@@ -186,15 +165,6 @@
     auto out = static_cast<buttons_input_rpt_t*>(data);
     *out = input_rpt;
 
-    // Reenable interrupts.
-    for (uint32_t i = 0; i < gpios_.size(); ++i) {
-        if (gpios_[i].config.type == BUTTONS_GPIO_TYPE_INTERRUPT) {
-            zx_status_t status = ConfigureInterrupt(i, PORT_KEY_INTERRUPT_START + i);
-            if (status != ZX_OK) {
-                return status;
-            }
-        }
-    }
     return ZX_OK;
 }
 
@@ -273,62 +243,54 @@
         return status;
     }
 
-    pdev_device_info_t pdev_info;
-    status = pdev_get_device_info(&pdev, &pdev_info);
+    // Get buttons metadata.
+    size_t actual = 0;
+    status = device_get_metadata_size(parent_, DEVICE_METADATA_BUTTONS_BUTTONS, &actual);
     if (status != ZX_OK) {
-        zxlogf(ERROR, "%s pdev_get_device_info failed %d\n", __FUNCTION__, status);
-        return status;
+        zxlogf(ERROR, "%s device_get_metadata_size failed %d\n", __FILE__, status);
+        return ZX_OK;
     }
-
-    // TODO(andresoportus): Remove BUTTONS_ID_MAX usage below once we add metadata size probe
-    // capability to devmgr.
-
+    size_t n_buttons = actual / sizeof(buttons_button_config_t);
     fbl::AllocChecker ac;
-    // We have up to BUTTONS_ID_MAX available buttons.
-    auto buttons = fbl::Array(new (&ac) buttons_button_config_t[BUTTONS_ID_MAX], BUTTONS_ID_MAX);
+    buttons_ = fbl::Array(new (&ac) buttons_button_config_t[n_buttons], n_buttons);
     if (!ac.check()) {
         return ZX_ERR_NO_MEMORY;
     }
-    size_t actual = 0;
-    status = device_get_metadata(parent_, DEVICE_METADATA_BUTTONS_BUTTONS, buttons.get(),
-                                 buttons.size() * sizeof(buttons_button_config_t), &actual);
-    if (status != ZX_OK) {
+    actual = 0;
+    status = device_get_metadata(parent_, DEVICE_METADATA_BUTTONS_BUTTONS, buttons_.get(),
+                                 buttons_.size() * sizeof(buttons_button_config_t), &actual);
+    if (status != ZX_OK || actual != buttons_.size() * sizeof(buttons_button_config_t)) {
         zxlogf(ERROR, "%s device_get_metadata failed %d\n", __FILE__, status);
         return status;
     }
-    size_t n_buttons = actual / sizeof(buttons_button_config_t);
 
-    // We have up to BUTTONS_ID_MAX available gpios.
-    auto gpios_configs = fbl::Array(
-        new (&ac) buttons_gpio_config_t[BUTTONS_ID_MAX], BUTTONS_ID_MAX);
+    // Get gpios metadata.
+    actual = 0;
+    status = device_get_metadata_size(parent_, DEVICE_METADATA_BUTTONS_GPIOS, &actual);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s device_get_metadata_size failed %d\n", __FILE__, status);
+        return ZX_OK;
+    }
+    size_t n_gpios = actual / sizeof(buttons_gpio_config_t);
+    auto gpios_configs = fbl::Array(new (&ac) buttons_gpio_config_t[n_gpios], n_gpios);
     if (!ac.check()) {
         return ZX_ERR_NO_MEMORY;
     }
     actual = 0;
     status = device_get_metadata(parent_, DEVICE_METADATA_BUTTONS_GPIOS, gpios_configs.get(),
                                  gpios_configs.size() * sizeof(buttons_gpio_config_t), &actual);
-    if (status != ZX_OK) {
+    if (status != ZX_OK || actual != gpios_configs.size() * sizeof(buttons_gpio_config_t)) {
         zxlogf(ERROR, "%s device_get_metadata failed %d\n", __FILE__, status);
         return status;
     }
-    size_t n_gpios = actual / sizeof(buttons_gpio_config_t);
 
-    buttons_ = fbl::Array(new (&ac) buttons_button_config_t[n_buttons], n_buttons);
-    if (!ac.check()) {
-        return ZX_ERR_NO_MEMORY;
-    }
-    gpios_ = fbl::Array(new (&ac) Gpio[n_gpios], n_gpios);
-    if (!ac.check()) {
-        return ZX_ERR_NO_MEMORY;
-    }
-
+    // Check the metadata.
     for (uint32_t i = 0; i < buttons_.size(); ++i) {
-        buttons_[i] = buttons[i];
-        if (buttons_[i].gpioA_idx >= gpios_.size()) {
+        if (buttons_[i].gpioA_idx >= gpios_configs.size()) {
             zxlogf(ERROR, "%s invalid gpioA_idx %u\n", __FUNCTION__, buttons_[i].gpioA_idx);
             return ZX_ERR_INTERNAL;
         }
-        if (buttons_[i].gpioB_idx >= gpios_.size()) {
+        if (buttons_[i].gpioB_idx >= gpios_configs.size()) {
             zxlogf(ERROR, "%s invalid gpioB_idx %u\n", __FUNCTION__, buttons_[i].gpioB_idx);
             return ZX_ERR_INTERNAL;
         }
@@ -349,12 +311,17 @@
         }
     }
 
+    // Prepare the GPIOs.
+    gpios_ = fbl::Array(new (&ac) Gpio[n_gpios], n_gpios);
+    if (!ac.check()) {
+        return ZX_ERR_NO_MEMORY;
+    }
     for (uint32_t i = 0; i < gpios_.size(); ++i) {
         gpios_[i].config = gpios_configs[i];
         size_t actual;
-        status = pdev_get_protocol(&pdev, ZX_PROTOCOL_GPIO, i, &gpios_[i].gpio,
-                                   sizeof(gpios_[i].gpio), &actual);
-        if (status != ZX_OK) {
+        status = PdevGetGpioProtocol(&pdev, i, &gpios_[i].gpio,
+                                     sizeof(gpios_[i].gpio), &actual);
+        if (status != ZX_OK || actual != sizeof(gpios_[i].gpio)) {
             zxlogf(ERROR, "%s pdev_get_protocol failed %d\n", __FUNCTION__, status);
             return ZX_ERR_NOT_SUPPORTED;
         }
@@ -375,7 +342,7 @@
                 zxlogf(ERROR, "%s gpio_config_in failed %d\n", __FUNCTION__, status);
                 return ZX_ERR_NOT_SUPPORTED;
             }
-            status = ConfigureInterrupt(i, PORT_KEY_INTERRUPT_START + i);
+            status = ConfigureInterrupt(i, kPortKeyInterruptStart + i);
             if (status != ZX_OK) {
                 return status;
             }
@@ -399,7 +366,7 @@
 }
 
 void HidButtonsDevice::ShutDown() {
-    zx_port_packet packet = {PORT_KEY_SHUTDOWN, ZX_PKT_TYPE_USER, ZX_OK, {}};
+    zx_port_packet packet = {kPortKeyShutDown, ZX_PKT_TYPE_USER, ZX_OK, {}};
     zx_status_t status = port_.queue(&packet);
     ZX_ASSERT(status == ZX_OK);
     thrd_join(thread_, NULL);
diff --git a/zircon/system/dev/input/hid-buttons/hid-buttons.h b/zircon/system/dev/input/hid-buttons/hid-buttons.h
index ca3ae4b..ad988a7 100644
--- a/zircon/system/dev/input/hid-buttons/hid-buttons.h
+++ b/zircon/system/dev/input/hid-buttons/hid-buttons.h
@@ -6,24 +6,25 @@
 
 #include <ddk/metadata/buttons.h>
 #include <ddk/protocol/gpio.h>
-
+#include <ddk/protocol/platform/device.h>
 #include <ddktl/device.h>
 #include <ddktl/protocol/hidbus.h>
-
 #include <fbl/array.h>
 #include <fbl/mutex.h>
-
 #include <hid/buttons.h>
-
 #include <lib/zx/interrupt.h>
 #include <lib/zx/port.h>
-
 #include <zircon/thread_annotations.h>
 
 #include <optional>
 
 namespace buttons {
 
+// zx_port_packet::key.
+constexpr uint64_t kPortKeyShutDown = 0x01;
+// Start of up to kNumberOfRequiredGpios port types used for interrupts.
+constexpr uint64_t kPortKeyInterruptStart = 0x10;
+
 class HidButtonsDevice;
 using DeviceType = ddk::Device<HidButtonsDevice, ddk::Unbindable>;
 
@@ -32,6 +33,7 @@
 public:
     explicit HidButtonsDevice(zx_device_t* device)
         : DeviceType(device) {}
+    virtual ~HidButtonsDevice() = default;
 
     zx_status_t Bind();
 
@@ -51,6 +53,11 @@
     void DdkUnbind();
     void DdkRelease();
 
+protected:
+    // Protected for unit testing.
+    zx::port port_;
+    void ShutDown() TA_EXCL(client_lock_);
+
 private:
     struct Gpio {
         gpio_protocol_t gpio;
@@ -59,17 +66,22 @@
     };
 
     int Thread();
-    void ShutDown() TA_EXCL(client_lock_);
     void ReconfigurePolarity(uint32_t idx, uint64_t int_port);
     zx_status_t ConfigureInterrupt(uint32_t idx, uint64_t int_port);
     bool MatrixScan(uint32_t row, uint32_t col, zx_duration_t delay);
+    // To be overwritten in unit testing.
+    virtual zx_status_t PdevGetGpioProtocol(const pdev_protocol_t* proto, uint32_t index,
+                                            void* out_protocol_buffer, size_t out_protocol_size,
+                                            size_t* out_protocol_actual) {
+        return pdev_get_protocol(proto, ZX_PROTOCOL_GPIO, index, out_protocol_buffer,
+                                 out_protocol_size, out_protocol_actual);
+    }
 
     thrd_t thread_;
-    zx::port port_;
     fbl::Mutex client_lock_;
     ddk::HidbusIfcProtocolClient client_ TA_GUARDED(client_lock_);
     fbl::Array<buttons_button_config_t> buttons_;
     fbl::Array<Gpio> gpios_;
     std::optional<uint8_t> fdr_gpio_;
 };
-}
+} // namespace buttons
diff --git a/zircon/system/dev/lib/fake_ddk/fake_ddk.cpp b/zircon/system/dev/lib/fake_ddk/fake_ddk.cpp
index 49fb598..0ed0267 100644
--- a/zircon/system/dev/lib/fake_ddk/fake_ddk.cpp
+++ b/zircon/system/dev/lib/fake_ddk/fake_ddk.cpp
@@ -106,6 +106,14 @@
     return ZX_OK;
 }
 
+zx_status_t Bind::DeviceGetMetadataSize(zx_device_t* dev, uint32_t type, size_t* out_size) {
+    if (get_metadata_ == nullptr) {
+        return ZX_ERR_BAD_STATE;
+    }
+    *out_size = get_metadata_length_;
+    return ZX_OK;
+}
+
 void Bind::DeviceMakeVisible(zx_device_t* device) {
     if (device != kFakeDevice) {
         bad_device_ = true;
@@ -205,6 +213,13 @@
     return fake_ddk::Bind::Instance()->DeviceGetMetadata(device, type, buf, buflen, actual);
 }
 
+zx_status_t device_get_metadata_size(zx_device_t* device, uint32_t type, size_t* out_size) {
+    if (!fake_ddk::Bind::Instance()) {
+        return ZX_ERR_NOT_SUPPORTED;
+    }
+    return fake_ddk::Bind::Instance()->DeviceGetMetadataSize(device, type, out_size);
+}
+
 extern "C" void driver_printf(uint32_t flags, const char* fmt, ...) {}
 
 __WEAK zx_driver_rec __zircon_driver_rec__ = {};
diff --git a/zircon/system/dev/lib/fake_ddk/include/lib/fake_ddk/fake_ddk.h b/zircon/system/dev/lib/fake_ddk/include/lib/fake_ddk/fake_ddk.h
index 29d3cc9..b3d774f 100644
--- a/zircon/system/dev/lib/fake_ddk/include/lib/fake_ddk/fake_ddk.h
+++ b/zircon/system/dev/lib/fake_ddk/include/lib/fake_ddk/fake_ddk.h
@@ -81,6 +81,9 @@
                                           size_t length, size_t* actual);
 
     // Internal fake implementation of ddk functionality.
+    virtual zx_status_t DeviceGetMetadataSize(zx_device_t* dev, uint32_t type, size_t* out_size);
+
+    // Internal fake implementation of ddk functionality.
     virtual void DeviceMakeVisible(zx_device_t* dev);
 
     // Internal fake implementation of ddk functionality.
diff --git a/zircon/system/utest/BUILD.gn b/zircon/system/utest/BUILD.gn
index 2e41924..3511cae 100644
--- a/zircon/system/utest/BUILD.gn
+++ b/zircon/system/utest/BUILD.gn
@@ -45,6 +45,7 @@
       "$zx/system/dev/gpio/qcom-gpio:qcom-gpio-test",
       "$zx/system/dev/i2c/mt8167-i2c:mt8167-i2c-test",
       "$zx/system/dev/input/bma253:bma253-test",
+      "$zx/system/dev/input/hid-buttons:hid-buttons-test",
       "$zx/system/dev/lib/scsi:scsilib-disk-test",
       "$zx/system/dev/lib/usb:usb-wrapper-test",
       "$zx/system/dev/light-sensor/lite-on:ltr-578als-test",