diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f57972..be71304 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: windows-2022 steps: - name: checkout project uses: actions/checkout@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 13f1163..feea3b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: windows-2022 steps: - name: checkout project uses: actions/checkout@v4 diff --git a/TestComponent/AsyncActionSource.cpp b/TestComponent/AsyncActionSource.cpp new file mode 100644 index 0000000..4c118e1 --- /dev/null +++ b/TestComponent/AsyncActionSource.cpp @@ -0,0 +1,11 @@ +#include "pch.h" +#include "AsyncActionSource.h" +#include "AsyncActionSource.g.cpp" + +namespace winrt::TestComponent::implementation +{ + void AsyncActionSource::Complete() + { + finish(Windows::Foundation::AsyncStatus::Completed, {}, 0); + } +} diff --git a/TestComponent/AsyncActionSource.h b/TestComponent/AsyncActionSource.h new file mode 100644 index 0000000..007ffd6 --- /dev/null +++ b/TestComponent/AsyncActionSource.h @@ -0,0 +1,21 @@ +#pragma once +#include "AsyncActionSource.g.h" +#include "ControlledAsync.h" + +namespace winrt::TestComponent::implementation +{ + struct AsyncActionSource : AsyncActionSourceT, + controlled_async_source + { + AsyncActionSource() = default; + + void Complete(); + }; +} + +namespace winrt::TestComponent::factory_implementation +{ + struct AsyncActionSource : AsyncActionSourceT + { + }; +} diff --git a/TestComponent/AsyncOperationSource.cpp b/TestComponent/AsyncOperationSource.cpp new file mode 100644 index 0000000..bfb973f --- /dev/null +++ b/TestComponent/AsyncOperationSource.cpp @@ -0,0 +1,11 @@ +#include "pch.h" +#include "AsyncOperationSource.h" +#include "AsyncOperationSource.g.cpp" + +namespace winrt::TestComponent::implementation +{ + void AsyncOperationSource::Complete(int32_t result) + { + finish(Windows::Foundation::AsyncStatus::Completed, {}, result); + } +} diff --git a/TestComponent/AsyncOperationSource.h b/TestComponent/AsyncOperationSource.h new file mode 100644 index 0000000..5effca9 --- /dev/null +++ b/TestComponent/AsyncOperationSource.h @@ -0,0 +1,21 @@ +#pragma once +#include "AsyncOperationSource.g.h" +#include "ControlledAsync.h" + +namespace winrt::TestComponent::implementation +{ + struct AsyncOperationSource : AsyncOperationSourceT, + controlled_async_source, Windows::Foundation::AsyncOperationCompletedHandler> + { + AsyncOperationSource() = default; + + void Complete(int32_t result); + }; +} + +namespace winrt::TestComponent::factory_implementation +{ + struct AsyncOperationSource : AsyncOperationSourceT + { + }; +} diff --git a/TestComponent/ControlledAsync.h b/TestComponent/ControlledAsync.h new file mode 100644 index 0000000..ed93b46 --- /dev/null +++ b/TestComponent/ControlledAsync.h @@ -0,0 +1,276 @@ +#pragma once + +#include +#include + +// An async operation that finishes only when its source says so, and the +// source that says so. AsyncActionSource and AsyncOperationSource are the two +// instances of it that TestComponent exposes. + +namespace winrt::TestComponent::implementation +{ + // What an operation and its source both read and write. + struct controlled_async_state + { + slim_mutex lock; + Windows::Foundation::AsyncStatus status{ Windows::Foundation::AsyncStatus::Started }; + hresult error{}; + int32_t result{}; + uint32_t cancel_requests{}; + bool defers_handler{}; + bool handler_released{}; + }; + + // An IAsyncAction or IAsyncOperation that keeps its completed + // handler for as long as it exists, as C++/WinRT's coroutines do. + template + struct controlled_async : implements, Async, Windows::Foundation::IAsyncInfo> + { + explicit controlled_async(std::shared_ptr state) : + m_state(std::move(state)) + { + } + + uint32_t Id() const noexcept + { + return 1; + } + + Windows::Foundation::AsyncStatus Status() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->status; + } + + hresult ErrorCode() + { + slim_lock_guard const guard{ m_state->lock }; + + switch (m_state->status) + { + case Windows::Foundation::AsyncStatus::Error: + return m_state->error; + case Windows::Foundation::AsyncStatus::Canceled: + return impl::error_canceled; + default: + return {}; + } + } + + // A request, which the operation answers when its source finishes it. + void Cancel() + { + slim_lock_guard const guard{ m_state->lock }; + m_state->cancel_requests++; + } + + void Close() + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_state->status == Windows::Foundation::AsyncStatus::Started) + { + throw hresult_illegal_state_change(); + } + } + + void Completed(Handler const& handler) + { + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_completed) + { + throw hresult_illegal_delegate_assignment(); + } + + m_completed = handler; + } + + // An operation that has already finished calls its handler at once. + invoke_completed(); + } + + Handler Completed() + { + slim_lock_guard const guard{ m_state->lock }; + return m_completed; + } + + auto GetResults() + { + slim_lock_guard const guard{ m_state->lock }; + + switch (m_state->status) + { + case Windows::Foundation::AsyncStatus::Error: + throw hresult_error(m_state->error); + case Windows::Foundation::AsyncStatus::Canceled: + throw hresult_canceled(); + case Windows::Foundation::AsyncStatus::Started: + throw hresult_illegal_method_call(); + default: + break; + } + + if constexpr (!std::is_same_v) + { + return m_state->result; + } + } + + bool has_completed_handler() + { + slim_lock_guard const guard{ m_state->lock }; + return static_cast(m_completed); + } + + // Calls the completed handler once the operation has finished, it has + // a handler and the source is not holding the call back, and only once. + void invoke_completed() + { + Handler handler; + Windows::Foundation::AsyncStatus status{}; + + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_invoked || !m_completed) + { + return; + } + + if (m_state->status == Windows::Foundation::AsyncStatus::Started) + { + return; + } + + if (m_state->defers_handler && !m_state->handler_released) + { + return; + } + + m_invoked = true; + handler = m_completed; + status = m_state->status; + } + + handler(*this, status); + } + + private: + std::shared_ptr m_state; + Handler m_completed; + bool m_invoked{}; + }; + + // The members AsyncActionSource and AsyncOperationSource share. The source + // holds the operation only weakly, so that a test can see when the + // consumer has let go of it. + template + struct controlled_async_source + { + using operation_type = controlled_async; + + Async Operation() + { + slim_lock_guard const guard{ m_lock }; + + if (auto operation = m_operation.get()) + { + return operation.template as(); + } + + if (m_made) + { + throw hresult_illegal_method_call(L"the operation has been released"); + } + + auto operation = make_self(m_state); + m_operation = operation->get_weak(); + m_made = true; + + return operation.template as(); + } + + bool IsOperationAlive() + { + return static_cast(m_operation.get()); + } + + uint32_t CancelRequestCount() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->cancel_requests; + } + + bool HasCompletedHandler() + { + auto operation = m_operation.get(); + return operation && operation->has_completed_handler(); + } + + bool DefersCompletedHandler() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->defers_handler; + } + + void DefersCompletedHandler(bool value) + { + slim_lock_guard const guard{ m_state->lock }; + m_state->defers_handler = value; + } + + void Fail(int32_t error) + { + finish(Windows::Foundation::AsyncStatus::Error, hresult(error), 0); + } + + void Cancel() + { + finish(Windows::Foundation::AsyncStatus::Canceled, {}, 0); + } + + void InvokeCompletedHandler() + { + { + slim_lock_guard const guard{ m_state->lock }; + m_state->handler_released = true; + } + + if (auto operation = m_operation.get()) + { + operation->invoke_completed(); + } + } + + protected: + void finish(Windows::Foundation::AsyncStatus status, hresult error, int32_t result) + { + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_state->status != Windows::Foundation::AsyncStatus::Started) + { + throw hresult_illegal_state_change(); + } + + m_state->status = status; + m_state->error = error; + m_state->result = result; + } + + if (auto operation = m_operation.get()) + { + operation->invoke_completed(); + } + } + + private: + slim_mutex m_lock; + std::shared_ptr m_state{ std::make_shared() }; + weak_ref m_operation; + bool m_made{}; + }; +} diff --git a/TestComponent/TestComponent.idl b/TestComponent/TestComponent.idl index 1de2180..e118866 100644 --- a/TestComponent/TestComponent.idl +++ b/TestComponent/TestComponent.idl @@ -359,4 +359,59 @@ namespace TestComponent overridable void OnOverridable(); event Windows.Foundation.EventHandler OverridableCalled; } + + // An async action whose every step the caller drives, for testing how a + // projection waits for an operation, cancels it and lets go of it. + // + // Cancel() on the operation only counts the request, as an operation that + // takes time to stop does. The operation finishes when the caller calls + // Complete(), Fail() or Cancel() here. + runtimeclass AsyncActionSource + { + AsyncActionSource(); + + // The operation, made the first time it is read. The source does not + // keep it alive. + Windows.Foundation.IAsyncAction Operation{ get; }; + + // Whether the operation still exists, which it does until every + // reference to it has been released. + Boolean IsOperationAlive{ get; }; + + // How many times Cancel() has been called on the operation. + UInt32 CancelRequestCount{ get; }; + + // Whether a completed handler has been set on the operation. + Boolean HasCompletedHandler{ get; }; + + // When true, finishing the operation changes its status but leaves the + // completed handler uncalled until InvokeCompletedHandler(). + Boolean DefersCompletedHandler{ get; set; }; + + // Finishes the operation as Completed, Error or Canceled. Canceled is + // what the operation does whether or not Cancel() was called on it. + void Complete(); + void Fail(Int32 error); + void Cancel(); + + // Calls the completed handler that DefersCompletedHandler held back. + void InvokeCompletedHandler(); + } + + // AsyncActionSource for an IAsyncOperation. + runtimeclass AsyncOperationSource + { + AsyncOperationSource(); + + Windows.Foundation.IAsyncOperation Operation{ get; }; + Boolean IsOperationAlive{ get; }; + UInt32 CancelRequestCount{ get; }; + Boolean HasCompletedHandler{ get; }; + Boolean DefersCompletedHandler{ get; set; }; + + void Complete(Int32 result); + void Fail(Int32 error); + void Cancel(); + void InvokeCompletedHandler(); + } } diff --git a/TestComponent/TestComponent.nuspec b/TestComponent/TestComponent.nuspec index e226573..ee54a53 100644 --- a/TestComponent/TestComponent.nuspec +++ b/TestComponent/TestComponent.nuspec @@ -2,7 +2,7 @@ PyWinRT.TestWinRT - 1.3.0 + 1.4.0 TestWinRT Microsoft MIT diff --git a/TestComponent/TestComponent.vcxproj b/TestComponent/TestComponent.vcxproj index 2fc3ca6..2008b88 100644 --- a/TestComponent/TestComponent.vcxproj +++ b/TestComponent/TestComponent.vcxproj @@ -147,6 +147,9 @@ + + + @@ -156,6 +159,8 @@ + + diff --git a/TestComponent/TestComponent.vcxproj.filters b/TestComponent/TestComponent.vcxproj.filters index 008d317..9cbe815 100644 --- a/TestComponent/TestComponent.vcxproj.filters +++ b/TestComponent/TestComponent.vcxproj.filters @@ -18,6 +18,8 @@ + + @@ -27,6 +29,9 @@ + + +