Files
utun2/lib/ble/tests/test_ble_winrt.cpp
T

308 lines
18 KiB
C++

/* WinRT без радио: старые/новые интерфейсы объявления, mailbox, изоляция link и отмена callbacks.
* Включаем backend для доступа к внутренним событиям; общий transport заменён наблюдателем. */
#include "../ble_winrt.cpp"
#include <cstdlib>
#define CHECK(value) do { if (!(value)) { DEBUG_ERROR(CAT, "CHECK failed line=%d: %s", __LINE__, #value); std::abort(); } } while (0)
struct ble_transport {
WinState* state = nullptr;
unsigned radio_failures = 0, link_failures = 0, ready_events = 0, advertising_events = 0;
ble_link_id closed = 0;
std::string reason;
};
extern "C" void ble_backend_closed(ble_transport* t, ble_link_id id, const char* reason) {
++t->link_failures; t->closed = id; t->reason = reason; t->state->close(id);
}
extern "C" void ble_backend_status(ble_transport* t, int ready, const char* reason) {
if (ready) { ++t->ready_events; return; }
++t->radio_failures; t->reason = reason;
while (!t->state->links.empty()) t->state->close(t->state->links.begin()->first);
}
/* Эти события не должны возникать в проверке mailbox. */
extern "C" uint64_t ble_backend_peer(ble_transport*, ble_link_id) { return 3; }
extern "C" ble_link_id ble_backend_incoming(ble_transport*, const ble_endpoint*) { CHECK(false); return 0; }
extern "C" void ble_backend_advertising(ble_transport* t, ble_state, const char*) { ++t->advertising_events; }
extern "C" void ble_backend_broadcast(ble_transport*, const ble_endpoint*, const uint8_t*, size_t, int) { CHECK(false); }
extern "C" void ble_backend_opened(ble_transport*, ble_link_id, unsigned) { CHECK(false); }
extern "C" void ble_backend_received(ble_transport*, ble_link_id, ble_wire_channel, const uint8_t*, size_t) { CHECK(false); }
extern "C" void ble_backend_sent(ble_transport*, ble_link_id, int) { CHECK(false); }
static std::shared_ptr<WinState> make_state(UASYNC* ua, ble_transport& owner) {
auto s = std::make_shared<WinState>(); s->ua = ua; s->owner = &owner; owner.state = s.get();
for (ble_link_id id : {7, 9}) {
auto l = std::make_unique<WinLink>(); l->id = id; s->links.emplace(id, std::move(l));
}
return s;
}
/* Устройство без Win11 API: успешное открытие и закрытие проверяются без радио/activation factory. */
struct TestSpeedDevice : implements<TestSpeedDevice, IBluetoothLEDevice, IClosable> {
unsigned closed = 0;
void Close() { ++closed; }
hstring DeviceId() { CHECK(false); return {}; }
hstring Name() { CHECK(false); return {}; }
Windows::Foundation::Collections::IVectorView<GattDeviceService> GattServices() { CHECK(false); return nullptr; }
BluetoothConnectionStatus ConnectionStatus() { CHECK(false); return BluetoothConnectionStatus::Connected; }
uint64_t BluetoothAddress() { CHECK(false); return 0; }
GattDeviceService GetGattService(guid) { CHECK(false); return nullptr; }
event_token NameChanged(const TypedEventHandler<BluetoothLEDevice, IInspectable>&) { CHECK(false); return {}; }
void NameChanged(event_token) noexcept { CHECK(false); }
event_token GattServicesChanged(const TypedEventHandler<BluetoothLEDevice, IInspectable>&) { CHECK(false); return {}; }
void GattServicesChanged(event_token) noexcept { CHECK(false); }
event_token ConnectionStatusChanged(const TypedEventHandler<BluetoothLEDevice, IInspectable>&) { CHECK(false); return {}; }
void ConnectionStatusChanged(event_token) noexcept { CHECK(false); }
};
/* Управляемая гонка Cancel/Completed и ошибки GetResults; Cancel не исключает уже завершившийся запрос. */
struct TestSpeedOperation : implements<TestSpeedOperation, IAsyncOperation<BluetoothLEDevice>, IAsyncInfo> {
AsyncOperationCompletedHandler<BluetoothLEDevice> handler{nullptr};
BluetoothLEDevice device{nullptr};
AsyncStatus status = AsyncStatus::Started;
unsigned cancelled = 0;
bool results_error = false;
uint32_t Id() { return 1; }
AsyncStatus Status() { return status; }
hresult ErrorCode() { return E_ACCESSDENIED; }
void Cancel() { ++cancelled; }
void Close() {}
void Completed(const AsyncOperationCompletedHandler<BluetoothLEDevice>& value) { handler = value; }
AsyncOperationCompletedHandler<BluetoothLEDevice> Completed() { return handler; }
BluetoothLEDevice GetResults() {
if (results_error) throw hresult_error(E_ACCESSDENIED, L"test speed device denied");
return device;
}
void finish(AsyncStatus value) {
status = value; CHECK(handler);
handler(get_strong().as<IAsyncOperation<BluetoothLEDevice>>(), value);
}
};
/* Настройка независима от DATA: отказ/NULL/исключение не закрывают link; успешный device освобождается при close. */
static void incoming_speed(UASYNC* ua) {
for (int outcome = 0; outcome < 4; ++outcome) {
ble_transport owner;
auto s = make_state(ua, owner);
auto speed = make_self<TestSpeedOperation>(), data = make_self<TestSpeedOperation>();
auto device = make_self<TestSpeedDevice>();
if (!outcome) speed->device = device.as<BluetoothLEDevice>();
speed->results_error = outcome == 3;
auto* l = s->link(7); l->opened = true; l->sending = true; l->operation = data.as<IAsyncInfo>();
s->tune_incoming(*l, speed.as<IAsyncOperation<BluetoothLEDevice>>());
CHECK(l->speed_opening && l->operation == data.as<IAsyncInfo>());
speed->finish(outcome == 1 ? AsyncStatus::Error : AsyncStatus::Completed);
CHECK(!l->device); // Windows completion ещё не обработан в uasync.
uasync_poll(ua, 0);
CHECK(s->link(7) == l && l->opened && l->sending && !l->speed_opening);
CHECK(bool(l->device) == !outcome && l->operation == data.as<IAsyncInfo>() && !data->cancelled);
CHECK(!s->failed && !owner.radio_failures && !owner.link_failures && s->link(9));
s->close(7);
CHECK(data->cancelled == 1 && !speed->cancelled && device->closed == unsigned(!outcome));
s->shutdown();
}
ble_transport owner;
auto s = make_state(ua, owner);
auto speed = make_self<TestSpeedOperation>(); auto device = make_self<TestSpeedDevice>();
speed->device = device.as<BluetoothLEDevice>();
s->tune_incoming(*s->link(7), speed.as<IAsyncOperation<BluetoothLEDevice>>());
s->close(7); CHECK(speed->cancelled == 1);
speed->finish(AsyncStatus::Completed); uasync_poll(ua, 0);
CHECK(device->closed == 1 && !s->link(7) && s->link(9) && !s->failed && !owner.link_failures);
auto pending = make_self<TestSpeedOperation>();
s->tune_incoming(*s->link(9), pending.as<IAsyncOperation<BluetoothLEDevice>>());
s->shutdown(); CHECK(pending->cancelled == 1);
pending->finish(AsyncStatus::Canceled); uasync_poll(ua, 0);
CHECK(s->events.empty() && !s->posted && !owner.radio_failures && !owner.link_failures);
DEBUG_INFO(CAT, "WinRT test PASS: incoming speed preserves DATA on success/failure, closes device and cancels late/shutdown work");
}
using AddressType = Windows::Devices::Bluetooth::BluetoothAddressType;
/* Настоящий QueryInterface: объект 1809 не реализует EventArgs2, объект 2004 реализует. */
template<typename... Extra>
struct TestReceivedArgs : implements<TestReceivedArgs<Extra...>, IBluetoothLEAdvertisementReceivedEventArgs, Extra...> {
uint64_t address = 0;
AddressType type = AddressType::Unspecified;
uint64_t BluetoothAddress() { return address; }
AddressType BluetoothAddressType() { return type; }
int16_t RawSignalStrengthInDBm() { CHECK(false); return 0; }
BluetoothLEAdvertisementType AdvertisementType() { CHECK(false); return BluetoothLEAdvertisementType::ConnectableUndirected; }
DateTime Timestamp() { CHECK(false); return {}; }
BluetoothLEAdvertisement Advertisement() { CHECK(false); return nullptr; }
IReference<int16_t> TransmitPowerLevelInDBm() { CHECK(false); return nullptr; }
bool IsAnonymous() { CHECK(false); return false; }
bool IsConnectable() { CHECK(false); return false; }
bool IsScannable() { CHECK(false); return false; }
bool IsDirected() { CHECK(false); return false; }
bool IsScanResponse() { CHECK(false); return false; }
};
/* Отсутствие свойства не ломает radio и не подменяет неизвестный тип на public/random. */
static void advertisement_addresses(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
auto legacy = make_self<TestReceivedArgs<>>();
legacy->address = 0xc123456789ab;
auto args = legacy.as<BluetoothLEAdvertisementReceivedEventArgs>();
CHECK(!args.try_as<IBluetoothLEAdvertisementReceivedEventArgs2>());
unsigned processed = 0;
s->post("Watcher.Received", 0, [args, &processed](WinState& state) {
auto ep = state.advertisement_endpoint(args);
CHECK(std::string(ep.id) == "c123456789ab/2"); ++processed;
});
/* Ведущие нули сохраняются; высокий бит адреса не используется для угадывания типа. */
auto leading_zero = make_self<TestReceivedArgs<>>(); leading_zero->address = 0x12345678;
args = leading_zero.as<BluetoothLEAdvertisementReceivedEventArgs>();
s->post("Watcher.Received", 0, [args, &processed](WinState& state) {
auto ep = state.advertisement_endpoint(args);
CHECK(std::string(ep.id) == "000012345678/2"); ++processed;
});
for (auto type : {AddressType::Public, AddressType::Random, AddressType::Unspecified}) {
auto modern = make_self<TestReceivedArgs<IBluetoothLEAdvertisementReceivedEventArgs2>>();
modern->address = legacy->address; modern->type = type;
args = modern.as<BluetoothLEAdvertisementReceivedEventArgs>();
s->post("Watcher.Received", 0, [args, type, &processed](WinState& state) {
auto ep = state.advertisement_endpoint(args);
CHECK(std::string(ep.id) == "c123456789ab/" + std::to_string(int(type))); ++processed;
});
}
uasync_poll(ua, 0);
CHECK(processed == 5 && !s->failed && owner.radio_failures == 0 && owner.link_failures == 0);
CHECK(s->link(7) && s->link(9));
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: 1809 events use unspecified type; 2004 events preserve public/random/unspecified");
}
struct TestProvider : implements<TestProvider, IGattServiceProvider> {
bool read_error = false;
GattLocalService Service() { CHECK(false); return nullptr; }
GattServiceProviderAdvertisementStatus AdvertisementStatus() {
if (read_error) throw hresult_error(E_ACCESSDENIED, L"test GATT status denied");
return GattServiceProviderAdvertisementStatus::Stopped;
}
void StartAdvertising() { CHECK(false); }
void StartAdvertising(const GattServiceProviderAdvertisingParameters&) { CHECK(false); }
void StopAdvertising() { CHECK(false); }
event_token AdvertisementStatusChanged(const TypedEventHandler<GattServiceProvider,
GattServiceProviderAdvertisementStatusChangedEventArgs>&) { CHECK(false); return {}; }
void AdvertisementStatusChanged(event_token) noexcept { CHECK(false); }
};
/* Снимок отличает ОС от задержанных событий; ошибка одного getter не мешает остальным и не останавливает radio. */
static void radio_snapshot(UASYNC* ua) {
ble_transport owner; auto s = make_state(ua, owner);
auto empty = s->radio_status(); CHECK(empty.service == -1 && empty.scan == -1);
auto provider = make_self<TestProvider>(); s->provider = provider.as<GattServiceProvider>(); s->service_ready = true;
auto actual = s->radio_status(); CHECK(actual.service == 1 && actual.scan == -1);
provider->read_error = true; actual = s->radio_status(); CHECK(actual.service == -2);
CHECK(!s->failed && !owner.radio_failures && !owner.link_failures && s->links.size() == 2 && s->service_ready);
s->provider = nullptr; s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: actual GATT/scan snapshot tolerates getter failures without changing links");
}
static void publication_lifecycle(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
s->scan_ready = true; s->ready(); CHECK(owner.ready_events == 0);
s->service_ready = true; s->ready(); s->ready(); CHECK(owner.ready_events == 1);
s->broadcast_enabled = true; s->broadcast_size = 3; std::memcpy(s->broadcast.data(), "abc", 3);
ble_backend backend{s};
CHECK(ble_backend_advertise(&backend, reinterpret_cast<const uint8_t*>("abc"), 3) == BLE_OK);
CHECK(!s->publication_deadline && owner.advertising_events == 0);
/* Подтверждение StopAdvertising/Success не должно закрывать существующие links или повторять READY. */
s->broadcast_enabled = false; s->broadcast_size = 0;
for (auto status : {GattServiceProviderAdvertisementStatus::Stopped, GattServiceProviderAdvertisementStatus::Aborted}) {
s->service_stop_pending = true; s->service_requested = false;
s->service_status(status, BluetoothError::Success);
CHECK(!s->failed && !s->service_ready && !s->service_stop_pending && s->links.size() == 2);
s->service_status(GattServiceProviderAdvertisementStatus::Started, BluetoothError::Success);
CHECK(s->service_ready && owner.ready_events == 1);
}
s->publication_deadline = get_time_us() - 1;
s->publication_timeout(); s->publication_timeout();
CHECK(s->failed && owner.radio_failures == 1 && owner.advertising_events >= 1 && s->links.empty());
CHECK(ble_backend_advertise(&backend, nullptr, 0) == BLE_NOT_READY);
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: readiness gate, unchanged payload, requested stop/restart, publication timeout and closed backend");
for (auto error : {BluetoothError::Success, BluetoothError::RadioNotAvailable}) {
ble_transport failed_owner;
auto state = make_state(ua, failed_owner);
state->service_stop_pending = error != BluetoothError::Success;
state->service_status(GattServiceProviderAdvertisementStatus::Aborted, error);
CHECK(state->failed && failed_owner.radio_failures == 1); state->shutdown();
}
}
/* Ошибка GATT одного пира не переводит radio в failed и не теряет событие другого пира. */
static void link_exception(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
bool other_processed = false;
s->post("test.ValueChanged", 7, [](WinState& state) {
state.step("test.CharacteristicValue", 7);
throw hresult_error(E_NOINTERFACE, L"test interface absent");
});
s->post("test.other_peer", 9, [&other_processed](WinState&) { other_processed = true; });
uasync_poll(ua, 0);
CHECK(owner.link_failures == 1 && owner.closed == 7 && owner.radio_failures == 0);
CHECK(!s->failed && !s->link(7) && s->link(9) && other_processed);
CHECK(owner.reason.find("phase=test.CharacteristicValue link=7 HRESULT=0x80004002") != std::string::npos);
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: link exception preserves other peer and exact HRESULT context");
}
/* Общая ошибка сохраняет точную операцию даже при выключенном TRACE и останавливает batch. */
static void radio_exception(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
bool processed = false;
s->post("test.Received", 0, [](WinState& state) {
state.step("test.BluetoothAddressType");
throw hresult_error(E_NOINTERFACE, L"test interface absent");
});
s->post("test.after_failure", 9, [&processed](WinState&) { processed = true; });
uasync_poll(ua, 0);
CHECK(owner.radio_failures == 1 && owner.link_failures == 0 && s->failed);
CHECK(s->links.empty() && !processed);
CHECK(owner.reason.find("phase=test.BluetoothAddressType link=0 HRESULT=0x80004002") != std::string::npos);
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: radio exception stops queued delivery with TRACE disabled");
}
/* shutdown отменяет posted_task, освобождает captures и отвергает позднее событие Windows. */
static void pending_shutdown(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
/* Таймер проходит реальный callback, перевзводится и затем отменяется вместе с mailbox. */
s->diagnostics_timer = uasync_set_timeout(ua, 0, s.get(), WinState::diagnostics_tick, "test_diagnostics");
CHECK(s->diagnostics_timer);
uasync_poll(ua, 0);
CHECK(s->diagnostics_timer);
auto payload = std::make_shared<int>(0);
std::weak_ptr<int> weak_payload = payload;
bool processed = false;
s->post("test.pending", 9, [payload, &processed](WinState&) { processed = true; });
payload.reset(); CHECK(!weak_payload.expired());
s->shutdown(); CHECK(weak_payload.expired() && !s->diagnostics_timer);
s->post("test.late", 9, [&processed](WinState&) { processed = true; });
CHECK(s->events.empty() && !s->posted);
std::weak_ptr<WinState> weak = s; s.reset();
uasync_poll(ua, 0);
CHECK(weak.expired() && !processed && owner.radio_failures == 0 && owner.link_failures == 0);
DEBUG_INFO(CAT, "WinRT test PASS: shutdown cancels diagnostics timer/queued work and rejects late callbacks");
}
int main() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
for (unsigned bit = 0; bit < 64; ++bit) {
uint64_t node = UINT64_C(1) << bit; char uuid[37]; CHECK(!ble_service_uuid_encode(uuid, node));
CHECK(service_node(guid(uuid)) == node);
}
CHECK(!service_node(guid(BLE_SERVICE_UUID_FILTER)) && !service_node(guid(BLE_RX_UUID)));
init_apartment(apartment_type::multi_threaded);
UASYNC* ua = uasync_create(); CHECK(ua);
incoming_speed(ua); advertisement_addresses(ua); radio_snapshot(ua); publication_lifecycle(ua);
link_exception(ua); radio_exception(ua); pending_shutdown(ua);
uasync_destroy(ua, 0); uninit_apartment();
return 0;
}