Skip to content

Commit 39ea80b

Browse files
authored
Merge pull request #20 from sharwell/update-harness
Update harness
2 parents 5f1e71d + cca2e26 commit 39ea80b

24 files changed

+886
-65
lines changed

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/AbstractIntegrationTest.cs

Lines changed: 162 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,65 @@
44
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests
55
{
66
using System;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
710
using System.Windows.Automation;
11+
using Microsoft.VisualStudio;
12+
using Microsoft.Win32.SafeHandles;
813
using Xunit;
14+
using IMessageFilter = Microsoft.VisualStudio.OLE.Interop.IMessageFilter;
15+
using INTERFACEINFO = Microsoft.VisualStudio.OLE.Interop.INTERFACEINFO;
16+
using PENDINGMSG = Microsoft.VisualStudio.OLE.Interop.PENDINGMSG;
17+
using SERVERCALL = Microsoft.VisualStudio.OLE.Interop.SERVERCALL;
918

1019
[CaptureTestName]
1120
[Collection(nameof(SharedIntegrationHostFixture))]
12-
public abstract class AbstractIntegrationTest : IDisposable
21+
public abstract class AbstractIntegrationTest : IAsyncLifetime, IDisposable
1322
{
23+
private readonly MessageFilter _messageFilter;
24+
private readonly VisualStudioInstanceFactory _instanceFactory;
25+
private readonly Version _version;
1426
private VisualStudioInstanceContext _visualStudioContext;
1527

1628
protected AbstractIntegrationTest(VisualStudioInstanceFactory instanceFactory, Version version)
1729
{
18-
Automation.TransactionTimeout = 20000;
19-
_visualStudioContext = instanceFactory.GetNewOrUsedInstance(version, SharedIntegrationHostFixture.RequiredPackageIds);
20-
VisualStudio = _visualStudioContext.Instance;
30+
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
31+
32+
// Install a COM message filter to handle retry operations when the first attempt fails
33+
_messageFilter = RegisterMessageFilter();
34+
_instanceFactory = instanceFactory;
35+
_version = version;
36+
37+
try
38+
{
39+
Automation.TransactionTimeout = 20000;
40+
}
41+
catch
42+
{
43+
_messageFilter.Dispose();
44+
throw;
45+
}
2146
}
2247

23-
public VisualStudioInstance VisualStudio
48+
public VisualStudioInstance VisualStudio => _visualStudioContext?.Instance;
49+
50+
public virtual async Task InitializeAsync()
2451
{
25-
get;
52+
try
53+
{
54+
_visualStudioContext = await _instanceFactory.GetNewOrUsedInstanceAsync(_version, SharedIntegrationHostFixture.RequiredPackageIds).ConfigureAwait(false);
55+
}
56+
catch
57+
{
58+
_messageFilter.Dispose();
59+
throw;
60+
}
61+
}
62+
63+
public Task DisposeAsync()
64+
{
65+
return Task.FromResult<object>(null);
2666
}
2767

2868
public void Dispose()
@@ -31,11 +71,126 @@ public void Dispose()
3171
GC.SuppressFinalize(this);
3272
}
3373

74+
protected virtual MessageFilter RegisterMessageFilter()
75+
=> new MessageFilter();
76+
3477
protected virtual void Dispose(bool disposing)
3578
{
3679
if (disposing)
3780
{
38-
_visualStudioContext.Dispose();
81+
try
82+
{
83+
_visualStudioContext?.Dispose();
84+
}
85+
finally
86+
{
87+
_messageFilter.Dispose();
88+
}
89+
}
90+
}
91+
92+
protected class MessageFilter : IMessageFilter, IDisposable
93+
{
94+
protected const uint CancelCall = ~0U;
95+
96+
private readonly MessageFilterSafeHandle _messageFilterRegistration;
97+
private readonly TimeSpan _timeout;
98+
private readonly TimeSpan _retryDelay;
99+
100+
public MessageFilter()
101+
: this(timeout: TimeSpan.FromSeconds(60), retryDelay: TimeSpan.FromMilliseconds(150))
102+
{
103+
}
104+
105+
public MessageFilter(TimeSpan timeout, TimeSpan retryDelay)
106+
{
107+
_timeout = timeout;
108+
_retryDelay = retryDelay;
109+
_messageFilterRegistration = MessageFilterSafeHandle.Register(this);
110+
}
111+
112+
public virtual uint HandleInComingCall(uint dwCallType, IntPtr htaskCaller, uint dwTickCount, INTERFACEINFO[] lpInterfaceInfo)
113+
{
114+
return (uint)SERVERCALL.SERVERCALL_ISHANDLED;
115+
}
116+
117+
public virtual uint RetryRejectedCall(IntPtr htaskCallee, uint dwTickCount, uint dwRejectType)
118+
{
119+
if ((SERVERCALL)dwRejectType != SERVERCALL.SERVERCALL_RETRYLATER
120+
&& (SERVERCALL)dwRejectType != SERVERCALL.SERVERCALL_REJECTED)
121+
{
122+
return CancelCall;
123+
}
124+
125+
if (dwTickCount >= _timeout.TotalMilliseconds)
126+
{
127+
return CancelCall;
128+
}
129+
130+
return (uint)_retryDelay.TotalMilliseconds;
131+
}
132+
133+
public virtual uint MessagePending(IntPtr htaskCallee, uint dwTickCount, uint dwPendingType)
134+
{
135+
return (uint)PENDINGMSG.PENDINGMSG_WAITDEFPROCESS;
136+
}
137+
138+
protected virtual void Dispose(bool disposing)
139+
{
140+
if (disposing)
141+
{
142+
_messageFilterRegistration.Dispose();
143+
}
144+
}
145+
146+
public void Dispose()
147+
{
148+
Dispose(true);
149+
GC.SuppressFinalize(this);
150+
}
151+
}
152+
153+
private sealed class MessageFilterSafeHandle : SafeHandleMinusOneIsInvalid
154+
{
155+
private readonly IntPtr _oldFilter;
156+
157+
private MessageFilterSafeHandle(IntPtr handle)
158+
: base(true)
159+
{
160+
SetHandle(handle);
161+
162+
try
163+
{
164+
if (CoRegisterMessageFilter(handle, out _oldFilter) != VSConstants.S_OK)
165+
{
166+
throw new InvalidOperationException("Failed to register a new message filter");
167+
}
168+
}
169+
catch
170+
{
171+
SetHandleAsInvalid();
172+
throw;
173+
}
174+
}
175+
176+
[DllImport("ole32", SetLastError = true)]
177+
private static extern int CoRegisterMessageFilter(IntPtr messageFilter, out IntPtr oldMessageFilter);
178+
179+
public static MessageFilterSafeHandle Register<T>(T messageFilter)
180+
where T : IMessageFilter
181+
{
182+
var handle = Marshal.GetComInterfaceForObject<T, IMessageFilter>(messageFilter);
183+
return new MessageFilterSafeHandle(handle);
184+
}
185+
186+
protected override bool ReleaseHandle()
187+
{
188+
if (CoRegisterMessageFilter(_oldFilter, out _) == VSConstants.S_OK)
189+
{
190+
Marshal.Release(handle);
191+
}
192+
193+
return true;
39194
}
40195
}
41196
}

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/InProcComponent.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ private static Dispatcher CurrentApplicationDispatcher
3131
=> Application.Current.Dispatcher;
3232

3333
protected static void BeginInvokeOnUIThread(Action action)
34-
=> CurrentApplicationDispatcher.BeginInvoke(action);
34+
=> CurrentApplicationDispatcher.BeginInvoke(action, DispatcherPriority.Background);
3535

3636
protected static void InvokeOnUIThread(Action action)
37-
=> CurrentApplicationDispatcher.Invoke(action);
37+
=> CurrentApplicationDispatcher.Invoke(action, DispatcherPriority.Background);
3838

3939
protected static T InvokeOnUIThread<T>(Func<T> action)
40-
=> CurrentApplicationDispatcher.Invoke(action);
40+
=> CurrentApplicationDispatcher.Invoke(action, DispatcherPriority.Background);
4141

4242
protected static TInterface GetGlobalService<TService, TInterface>()
4343
where TService : class

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/IntegrationHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public static void SetForegroundWindow(IntPtr window, bool skipAttachingThread =
233233
try
234234
{
235235
// No need to re-attach threads in case when VS initializaed an UI thread for a debugged application.
236-
if (!skipAttachingThread)
236+
if (!skipAttachingThread && activeThreadId != currentThreadId)
237237
{
238238
// Attach the thread inputs so that 'SetActiveWindow' and 'SetFocus' work
239239
threadInputsAttached = AttachThreadInput(currentThreadId, activeThreadId);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information.
3+
4+
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
5+
{
6+
using System;
7+
8+
public class ConditionalWpfFactAttribute : WpfFactAttribute
9+
{
10+
public ConditionalWpfFactAttribute(Type skipCondition)
11+
{
12+
var condition = Activator.CreateInstance(skipCondition) as ExecutionCondition;
13+
if (condition.ShouldSkip)
14+
{
15+
Skip = condition.SkipReason;
16+
}
17+
}
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information.
3+
4+
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
5+
{
6+
using System;
7+
8+
public class ConditionalWpfTheoryAttribute : WpfTheoryAttribute
9+
{
10+
public ConditionalWpfTheoryAttribute(Type skipCondition)
11+
{
12+
var condition = Activator.CreateInstance(skipCondition) as ExecutionCondition;
13+
if (condition.ShouldSkip)
14+
{
15+
Skip = condition.SkipReason;
16+
}
17+
}
18+
}
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information.
3+
4+
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
5+
{
6+
using System;
7+
8+
internal static class ExceptionUtilities
9+
{
10+
internal static Exception Unreachable
11+
=> new InvalidOperationException("This program location is thought to be unreachable.");
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information.
3+
4+
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
5+
{
6+
public abstract class ExecutionCondition
7+
{
8+
public abstract bool ShouldSkip { get; }
9+
10+
public abstract string SkipReason { get; }
11+
}
12+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information.
3+
4+
namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
5+
{
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
internal static class SemaphoreExtensions
11+
{
12+
public static SemaphoreDisposer DisposableWait(this Semaphore semaphore, CancellationToken cancellationToken)
13+
{
14+
if (cancellationToken.CanBeCanceled)
15+
{
16+
int signalledIndex = WaitHandle.WaitAny(new[] { semaphore, cancellationToken.WaitHandle });
17+
if (signalledIndex != 0)
18+
{
19+
cancellationToken.ThrowIfCancellationRequested();
20+
throw ExceptionUtilities.Unreachable;
21+
}
22+
}
23+
else
24+
{
25+
semaphore.WaitOne();
26+
}
27+
28+
return new SemaphoreDisposer(semaphore);
29+
}
30+
31+
public static Task<SemaphoreDisposer> DisposableWaitAsync(this Semaphore semaphore, CancellationToken cancellationToken)
32+
{
33+
return Task.Factory.StartNew(
34+
() => DisposableWait(semaphore, cancellationToken),
35+
cancellationToken,
36+
TaskCreationOptions.LongRunning,
37+
TaskScheduler.Default);
38+
}
39+
40+
internal struct SemaphoreDisposer : IDisposable
41+
{
42+
private readonly Semaphore _semaphore;
43+
44+
public SemaphoreDisposer(Semaphore semaphore)
45+
{
46+
_semaphore = semaphore;
47+
}
48+
49+
public void Dispose()
50+
{
51+
_semaphore.Release();
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)