/* WinRT без радио: старые/новые интерфейсы объявления, mailbox, изоляция link и отмена callbacks. * Включаем backend для доступа к внутренним событиям; общий transport заменён наблюдателем. */ #include "../ble_winrt.cpp" #include #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 make_state(UASYNC* ua, ble_transport& owner) { auto s = std::make_shared(); s->ua = ua; s->owner = &owner; owner.state = s.get(); for (ble_link_id id : {7, 9}) { auto l = std::make_unique(); l->id = id; s->links.emplace(id, std::move(l)); } return s; } /* Устройство без Win11 API: успешное открытие и закрытие проверяются без радио/activation factory. */ struct TestSpeedDevice : implements { unsigned closed = 0; void Close() { ++closed; } hstring DeviceId() { CHECK(false); return {}; } hstring Name() { CHECK(false); return {}; } Windows::Foundation::Collections::IVectorView 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&) { CHECK(false); return {}; } void NameChanged(event_token) noexcept { CHECK(false); } event_token GattServicesChanged(const TypedEventHandler&) { CHECK(false); return {}; } void GattServicesChanged(event_token) noexcept { CHECK(false); } event_token ConnectionStatusChanged(const TypedEventHandler&) { CHECK(false); return {}; } void ConnectionStatusChanged(event_token) noexcept { CHECK(false); } }; /* Управляемая гонка Cancel/Completed и ошибки GetResults; Cancel не исключает уже завершившийся запрос. */ struct TestSpeedOperation : implements, IAsyncInfo> { AsyncOperationCompletedHandler 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& value) { handler = value; } AsyncOperationCompletedHandler 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>(), 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(), data = make_self(); auto device = make_self(); if (!outcome) speed->device = device.as(); speed->results_error = outcome == 3; auto* l = s->link(7); l->opened = true; l->sending = true; l->operation = data.as(); s->tune_incoming(*l, speed.as>()); CHECK(l->speed_opening && l->operation == data.as()); 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() && !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(); auto device = make_self(); speed->device = device.as(); s->tune_incoming(*s->link(7), speed.as>()); 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(); s->tune_incoming(*s->link(9), pending.as>()); 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 struct TestReceivedArgs : implements, 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 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>(); legacy->address = 0xc123456789ab; auto args = legacy.as(); CHECK(!args.try_as()); 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>(); leading_zero->address = 0x12345678; args = leading_zero.as(); 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>(); modern->address = legacy->address; modern->type = type; args = modern.as(); 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 { 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&) { 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(); s->provider = provider.as(); 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("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(0); std::weak_ptr 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 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; }