blob: d06d03b5d5f090e16ad86f490cbbecb273f3ef84 [file] [log] [blame]
// 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 "src/connectivity/bluetooth/core/bt-host/sdp/service_discoverer.h"
#include <lib/async/default.h>
#include <gtest/gtest.h>
#include "src/connectivity/bluetooth/core/bt-host/common/test_helpers.h"
#include "src/connectivity/bluetooth/core/bt-host/l2cap/fake_channel.h"
#include "src/connectivity/bluetooth/core/bt-host/l2cap/fake_channel_test.h"
namespace bt::sdp {
namespace {
using TestingBase = ::gtest::TestLoopFixture;
constexpr PeerId kDeviceOne(1), kDeviceTwo(2), kDeviceThree(3);
class FakeClient : public Client {
public:
// |destroyed_cb| will be called when this client is destroyed, with true if
// there are outstanding expected requests.
FakeClient(fit::closure destroyed_cb) : destroyed_cb_(std::move(destroyed_cb)) {}
virtual ~FakeClient() override { destroyed_cb_(); }
virtual void ServiceSearchAttributes(std::unordered_set<UUID> search_pattern,
const std::unordered_set<AttributeId> &req_attributes,
SearchResultCallback result_cb,
async_dispatcher_t *cb_dispatcher) override {
if (!service_search_attributes_cb_) {
FAIL() << "ServiceSearchAttributes with no callback set";
}
service_search_attributes_cb_(std::move(search_pattern), std::move(req_attributes),
std::move(result_cb), cb_dispatcher);
}
using ServiceSearchAttributesCalllback =
fit::function<void(std::unordered_set<UUID>, std::unordered_set<AttributeId>,
SearchResultCallback, async_dispatcher_t *)>;
void SetServiceSearchAttributesCallback(ServiceSearchAttributesCalllback callback) {
service_search_attributes_cb_ = std::move(callback);
}
private:
ServiceSearchAttributesCalllback service_search_attributes_cb_;
fit::closure destroyed_cb_;
};
class SDP_ServiceDiscovererTest : public TestingBase {
public:
SDP_ServiceDiscovererTest() = default;
~SDP_ServiceDiscovererTest() = default;
protected:
void SetUp() override {
clients_created_ = 0;
clients_destroyed_ = 0;
}
void TearDown() override {}
// Connect an SDP client to a fake channel, which is available in channel_
std::unique_ptr<FakeClient> GetFakeClient() {
SCOPED_TRACE("Connect Client");
clients_created_++;
return std::make_unique<FakeClient>([this]() { clients_destroyed_++; });
}
size_t clients_created() const { return clients_created_; };
size_t clients_destroyed() const { return clients_destroyed_; };
private:
size_t clients_created_, clients_destroyed_;
};
// When there are no searches registered, it just disconnects the client.
TEST_F(SDP_ServiceDiscovererTest, NoSearches) {
ServiceDiscoverer discoverer;
EXPECT_EQ(0u, discoverer.search_count());
discoverer.StartServiceDiscovery(kDeviceOne, GetFakeClient());
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(1u, clients_destroyed());
}
// Happy path test with one registered service and no results.
TEST_F(SDP_ServiceDiscovererTest, NoResults) {
ServiceDiscoverer discoverer;
size_t cb_count = 0;
auto result_cb = [&cb_count](auto, const auto &) { cb_count++; };
ServiceDiscoverer::SearchId id = discoverer.AddSearch(
profile::kSerialPort, {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
std::move(result_cb));
ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
EXPECT_EQ(1u, discoverer.search_count());
auto client = GetFakeClient();
std::vector<std::unordered_set<UUID>> searches;
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(std::move(pattern));
async::PostTask(cb_dispatcher,
[cb = std::move(callback)]() { cb(Status(HostError::kNotFound), {}); });
});
discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(1u, searches.size());
ASSERT_EQ(0u, cb_count);
ASSERT_EQ(1u, clients_destroyed());
}
// Happy path test with two registered searches.
// No results, then two results.
// Unregister one search.
// Then one result are searched for and two returned.
TEST_F(SDP_ServiceDiscovererTest, SomeResults) {
ServiceDiscoverer discoverer;
std::vector<std::pair<PeerId, std::map<AttributeId, DataElement>>> results;
ServiceDiscoverer::ResultCallback result_cb = [&results](PeerId id, const auto &attributes) {
std::map<AttributeId, DataElement> attributes_clone;
for (const auto &it : attributes) {
auto [inserted_it, added] = attributes_clone.try_emplace(it.first, it.second.Clone());
ASSERT_TRUE(added);
}
results.emplace_back(id, std::move(attributes_clone));
};
ServiceDiscoverer::SearchId one = discoverer.AddSearch(
profile::kSerialPort, {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
result_cb.share());
ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, one);
EXPECT_EQ(1u, discoverer.search_count());
ServiceDiscoverer::SearchId two = discoverer.AddSearch(
profile::kAudioSink, {kProtocolDescriptorList, kBluetoothProfileDescriptorList},
result_cb.share());
ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, two);
EXPECT_EQ(2u, discoverer.search_count());
auto client = GetFakeClient();
std::vector<std::unordered_set<UUID>> searches;
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(std::move(pattern));
async::PostTask(cb_dispatcher,
[cb = std::move(callback)]() { cb(Status(HostError::kNotFound), {}); });
});
discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(2u, searches.size());
ASSERT_EQ(0u, results.size());
ASSERT_EQ(1u, clients_destroyed());
client = GetFakeClient();
searches.clear();
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(pattern);
if (pattern.count(profile::kSerialPort)) {
async::PostTask(cb_dispatcher, [cb = std::move(callback)]() {
ServiceSearchAttributeResponse rsp;
rsp.SetAttribute(0, kServiceId, DataElement(UUID(uint16_t{1})));
// This would normally be a element list. uint32_t for Testing.
rsp.SetAttribute(0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
if (!cb(Status(), rsp.attributes(0))) {
return;
}
cb(Status(HostError::kNotFound), {});
});
} else if (pattern.count(profile::kAudioSink)) {
async::PostTask(cb_dispatcher, [cb = std::move(callback)]() {
ServiceSearchAttributeResponse rsp;
// This would normally be a element list. uint32_t for Testing.
rsp.SetAttribute(0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
if (!cb(Status(), rsp.attributes(0))) {
return;
}
cb(Status(HostError::kNotFound), {});
});
} else {
std::cerr << "Searched for " << pattern.size() << std::endl;
for (auto it : pattern) {
std::cerr << it.ToString() << std::endl;
}
FAIL() << "Unexpected search called";
}
});
discoverer.StartServiceDiscovery(kDeviceTwo, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(2u, searches.size());
ASSERT_EQ(2u, results.size());
ASSERT_EQ(2u, clients_destroyed());
results.clear();
searches.clear();
ASSERT_TRUE(discoverer.RemoveSearch(one));
ASSERT_FALSE(discoverer.RemoveSearch(one));
EXPECT_EQ(1u, discoverer.search_count());
client = GetFakeClient();
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(pattern);
if (pattern.count(profile::kAudioSink)) {
async::PostTask(cb_dispatcher, [cb = std::move(callback)]() {
ServiceSearchAttributeResponse rsp;
// This would normally be a element list. uint32_t for Testing.
rsp.SetAttribute(0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
rsp.SetAttribute(1, kProtocolDescriptorList, DataElement(uint32_t{2}));
if (!cb(Status(), rsp.attributes(0))) {
return;
}
if (!cb(Status(), rsp.attributes(1))) {
return;
}
cb(Status(HostError::kNotFound), {});
});
} else {
std::cerr << "Searched for " << pattern.size() << std::endl;
for (auto it : pattern) {
std::cerr << it.ToString() << std::endl;
}
FAIL() << "Unexpected search called";
}
});
discoverer.StartServiceDiscovery(kDeviceThree, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(1u, searches.size());
ASSERT_EQ(2u, results.size());
ASSERT_EQ(3u, clients_destroyed());
}
// Disconnected on the other end before the discovery completes
TEST_F(SDP_ServiceDiscovererTest, Disconnected) {
ServiceDiscoverer discoverer;
size_t cb_count = 0;
auto result_cb = [&cb_count](auto, const auto &) { cb_count++; };
ServiceDiscoverer::SearchId id = discoverer.AddSearch(
profile::kSerialPort, {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
std::move(result_cb));
ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
EXPECT_EQ(1u, discoverer.search_count());
auto client = GetFakeClient();
std::vector<std::unordered_set<UUID>> searches;
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(pattern);
if (pattern.count(profile::kSerialPort)) {
async::PostTask(cb_dispatcher, [cb = std::move(callback)]() {
cb(Status(HostError::kLinkDisconnected), {});
});
} else {
std::cerr << "Searched for " << pattern.size() << std::endl;
for (auto it : pattern) {
std::cerr << it.ToString() << std::endl;
}
FAIL() << "Unexpected search called";
}
});
discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(1u, searches.size());
ASSERT_EQ(0u, cb_count);
ASSERT_EQ(1u, clients_destroyed());
}
// Unregistered Search when partway through the discovery
TEST_F(SDP_ServiceDiscovererTest, UnregisterInProgress) {
ServiceDiscoverer discoverer;
std::optional<std::pair<PeerId, std::map<AttributeId, DataElement>>> result;
ServiceDiscoverer::SearchId id = ServiceDiscoverer::kInvalidSearchId;
ServiceDiscoverer::ResultCallback one_result_cb = [&discoverer, &result, &id](
auto peer_id, const auto &attributes) {
// We should only be called once
ASSERT_TRUE(!result.has_value());
std::map<AttributeId, DataElement> attributes_clone;
for (const auto &it : attributes) {
auto [inserted_it, added] = attributes_clone.try_emplace(it.first, it.second.Clone());
ASSERT_TRUE(added);
}
result.emplace(peer_id, std::move(attributes_clone));
discoverer.RemoveSearch(id);
};
id = discoverer.AddSearch(profile::kAudioSink,
{kProtocolDescriptorList, kBluetoothProfileDescriptorList},
one_result_cb.share());
ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
EXPECT_EQ(1u, discoverer.search_count());
auto client = GetFakeClient();
std::vector<std::unordered_set<UUID>> searches;
client->SetServiceSearchAttributesCallback(
[&searches](auto pattern, auto attributes, auto callback, auto *cb_dispatcher) {
searches.emplace_back(pattern);
if (pattern.count(profile::kAudioSink)) {
async::PostTask(cb_dispatcher, [cb = std::move(callback)]() {
ServiceSearchAttributeResponse rsp;
// This would normally be a element list. uint32_t for Testing.
rsp.SetAttribute(0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
rsp.SetAttribute(1, kProtocolDescriptorList, DataElement(uint32_t{2}));
if (!cb(Status(), rsp.attributes(0))) {
return;
}
if (!cb(Status(), rsp.attributes(1))) {
return;
}
cb(Status(HostError::kNotFound), {});
});
} else {
std::cerr << "Searched for " << pattern.size() << std::endl;
for (auto it : pattern) {
std::cerr << it.ToString() << std::endl;
}
FAIL() << "Unexpected search called";
}
});
discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
RETURN_IF_FATAL(RunLoopUntilIdle());
EXPECT_EQ(1u, searches.size());
ASSERT_TRUE(result.has_value());
ASSERT_EQ(kDeviceOne, result->first);
auto value = result->second[kBluetoothProfileDescriptorList].Get<uint32_t>();
ASSERT_TRUE(value);
ASSERT_EQ(1u, *value);
ASSERT_EQ(1u, clients_destroyed());
EXPECT_EQ(0u, discoverer.search_count());
}
} // namespace
} // namespace bt::sdp