From 0ca951db625bd5932c59e060c97707bcaf02f3fe Mon Sep 17 00:00:00 2001 From: Rob Rolnick Date: Tue, 11 Nov 2025 23:57:59 -0800 Subject: [PATCH 1/4] Make WopiValidator Support Async/Await --- src/WopiValidator.Core/IWopiRequest.cs | 11 ++ .../Requests/GetFromFileUrlRequest.cs | 20 ++ .../Requests/RequestBase.cs | 116 ++++++++++- .../Requests/WopiRequest.cs | 26 ++- src/WopiValidator.Core/TestCaseExecutor.cs | 180 +++++++++++++++++- src/WopiValidator/Options.cs | 3 + src/WopiValidator/Program.cs | 36 ++-- 7 files changed, 369 insertions(+), 23 deletions(-) diff --git a/src/WopiValidator.Core/IWopiRequest.cs b/src/WopiValidator.Core/IWopiRequest.cs index d01a694..225cdce 100644 --- a/src/WopiValidator.Core/IWopiRequest.cs +++ b/src/WopiValidator.Core/IWopiRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Security.Cryptography; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator.Core { @@ -26,5 +27,15 @@ IResponseData Execute(string endpointAddress, string userAgent, RSACryptoServiceProvider proofKeyProviderNew, RSACryptoServiceProvider proofKeyProviderOld); + + Task ExecuteAsync(string endpointAddress, + string accessToken, + long accessTokenTtl, + ITestCase testCase, + Dictionary savedState, + IResourceManager resourceManager, + string userAgent, + RSACryptoServiceProvider proofKeyProviderNew, + RSACryptoServiceProvider proofKeyProviderOld); } } diff --git a/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs b/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs index 1ca9831..9e1f4d1 100644 --- a/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs +++ b/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator.Core.Requests { @@ -43,5 +44,24 @@ public override IResponseData Execute( return ExecuteRequest(executionData); } + + public override async Task ExecuteAsync( + string endpointAddress, + string accessToken, + long accessTokenTtl, + ITestCase testCase, + Dictionary savedState, + IResourceManager resourceManager, + string userAgent, + RSACryptoServiceProvider proofKeyProviderNew, + RSACryptoServiceProvider proofKeyProviderOld) + { + RequestExecutionData executionData = new RequestExecutionData( + new Uri(GetEndpointAddressOverride(savedState)), + Enumerable.Empty>(), + null); + + return await ExecuteRequestAsync(executionData); + } } } diff --git a/src/WopiValidator.Core/Requests/RequestBase.cs b/src/WopiValidator.Core/Requests/RequestBase.cs index ae1924d..5901f86 100644 --- a/src/WopiValidator.Core/Requests/RequestBase.cs +++ b/src/WopiValidator.Core/Requests/RequestBase.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Security.Cryptography; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator.Core.Requests { @@ -65,15 +66,7 @@ protected IResponseData ExecuteRequest( TargetUrl = executionData.TargetUri.AbsoluteUri; RequestHeaders = executionData.Headers.ToArray(); - HttpWebRequest request = WebRequest.CreateHttp(executionData.TargetUri); - request.UserAgent = userAgent; - request.AllowAutoRedirect = false; - - // apply custom headers - foreach (KeyValuePair header in RequestHeaders) - request.Headers.Add(header.Key, header.Value); - - request.Method = RequestMethod; + HttpWebRequest request = CreateHttpWebRequest(executionData, userAgent); MemoryStream content = executionData.ContentStream; // set proper ContentLength and content stream @@ -116,6 +109,77 @@ protected IResponseData ExecuteRequest( } } + /// + /// Executes request and gathers response data. + /// + /// URI request should be made against + /// Set of custom headers that should be added to the request + /// Request content stream + /// IResponseData instance with information takes from response. + protected async Task ExecuteRequestAsync( + RequestExecutionData executionData, + string userAgent = null + ) + { + TargetUrl = executionData.TargetUri.AbsoluteUri; + RequestHeaders = executionData.Headers.ToArray(); + + HttpWebRequest request = CreateHttpWebRequest(executionData, userAgent); + + MemoryStream content = executionData.ContentStream; + // set proper ContentLength and content stream + if (content != null) + { + request.ContentLength = content.Length; + using (Stream requestStream = await request.GetRequestStreamAsync()) + { + content.Seek(0, SeekOrigin.Begin); + await content.CopyToAsync(requestStream); + } + } + else + { + request.ContentLength = 0; + } + Stopwatch timer = new Stopwatch(); + try + { + timer.Start(); + using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync()) + { + timer.Stop(); + return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed); + } + } + // ProtocolErrors will have a non-null Response object so we can still get response details + catch (WebException ex) when (ex.Status == WebExceptionStatus.ProtocolError) + { + using (HttpWebResponse response = (HttpWebResponse)ex.Response) + { + timer.Stop(); + return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed); + } + } + // no response, so we wrap the exception details so they can be included in a validation failure + catch (WebException ex) + { + return ExceptionHelper.WrapExceptionInResponseData(ex); + } + } + + private HttpWebRequest CreateHttpWebRequest(RequestExecutionData executionData, string userAgent) + { + HttpWebRequest request = WebRequest.CreateHttp(executionData.TargetUri); + request.UserAgent = userAgent; + request.AllowAutoRedirect = false; + + // apply custom headers + foreach (KeyValuePair header in RequestHeaders) + request.Headers.Add(header.Key, header.Value); + + request.Method = RequestMethod; + return request; + } /// /// Replaces EndpointAddress if set @@ -176,6 +240,30 @@ private static IResponseData GetResponseData(HttpWebResponse response, bool isTe return new ResponseData(content, (int)response.StatusCode, headers, isTextResponseExpected, elapsed); } + /// + /// Gets information from the response. + /// + /// IResponseData instance with information from the response. + private static async Task GetResponseDataAsync(HttpWebResponse response, bool isTextResponseExpected, TimeSpan elapsed) + { + MemoryStream content = new MemoryStream(); + using (Stream responseStream = response.GetResponseStream()) + { + if (responseStream != null) + await responseStream.CopyToAsync(content); + } + + // just to be sure + content.Seek(0, SeekOrigin.Begin); + + Dictionary headers = response.Headers + .Cast() + .Select(k => new { Key = k, Value = response.Headers[ k ] }) + .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); + + return new ResponseData(content, (int)response.StatusCode, headers, isTextResponseExpected, elapsed); + } + public abstract IResponseData Execute(string endpointAddress, string accessToken, long accessTokenTtl, @@ -185,5 +273,15 @@ public abstract IResponseData Execute(string endpointAddress, string userAgent, RSACryptoServiceProvider proofKeyProviderNew, RSACryptoServiceProvider proofKeyProviderOld); + + public abstract Task ExecuteAsync(string endpointAddress, + string accessToken, + long accessTokenTtl, + ITestCase testCase, + Dictionary savedState, + IResourceManager resourceManager, + string userAgent, + RSACryptoServiceProvider proofKeyProviderNew, + RSACryptoServiceProvider proofKeyProviderOld); } } diff --git a/src/WopiValidator.Core/Requests/WopiRequest.cs b/src/WopiValidator.Core/Requests/WopiRequest.cs index 47322e4..23f4cf3 100644 --- a/src/WopiValidator.Core/Requests/WopiRequest.cs +++ b/src/WopiValidator.Core/Requests/WopiRequest.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator.Core.Requests { @@ -70,6 +71,29 @@ public override IResponseData Execute(string endpointAddress, string userAgent, RSACryptoServiceProvider proofKeyProviderNew, RSACryptoServiceProvider proofKeyProviderOld) + { + RequestExecutionData executionData = CreateExecutionData(endpointAddress, ref accessToken, accessTokenTtl, savedState, resourceManager, proofKeyProviderNew, proofKeyProviderOld); + return ExecuteRequest(executionData, userAgent); + } + + /// + /// Executes WOPI request at given WOPI endpoint address against provided wopi FileRep. + /// + public override async Task ExecuteAsync(string endpointAddress, + string accessToken, + long accessTokenTtl, + ITestCase testCase, + Dictionary savedState, + IResourceManager resourceManager, + string userAgent, + RSACryptoServiceProvider proofKeyProviderNew, + RSACryptoServiceProvider proofKeyProviderOld) + { + RequestExecutionData executionData = CreateExecutionData(endpointAddress, ref accessToken, accessTokenTtl, savedState, resourceManager, proofKeyProviderNew, proofKeyProviderOld); + return await ExecuteRequestAsync(executionData, userAgent); + } + + private RequestExecutionData CreateExecutionData(string endpointAddress, ref string accessToken, long accessTokenTtl, Dictionary savedState, IResourceManager resourceManager, RSACryptoServiceProvider proofKeyProviderNew, RSACryptoServiceProvider proofKeyProviderOld) { // Get the url of the WOPI endpoint that we'll call - either the normal endpoint, or a SavedState override. // If it's an override it might change the accessToken that we're using because it probably already has a token on it. @@ -105,7 +129,7 @@ public override IResponseData Execute(string endpointAddress, MemoryStream contentStream = HasRequestContent ? GetRequestContent(resourceManager) : null; RequestExecutionData executionData = new RequestExecutionData(uri, headers, contentStream); - return ExecuteRequest(executionData, userAgent); + return executionData; } protected Uri GetRequestUri(string endpointAddress, ref string accessToken, long accessTokenTtl, Dictionary savedState) diff --git a/src/WopiValidator.Core/TestCaseExecutor.cs b/src/WopiValidator.Core/TestCaseExecutor.cs index 0f7563a..406232f 100644 --- a/src/WopiValidator.Core/TestCaseExecutor.cs +++ b/src/WopiValidator.Core/TestCaseExecutor.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator.Core { @@ -61,6 +62,20 @@ public TestCaseResult Execute() return ExecuteTestCase(TestCase); } + public async Task ExecuteAsync() + { + foreach (var prereqCase in PrereqCases) + { + TestCaseResult prereqCaseResult = await ExecuteTestCaseAsync(prereqCase); + if (prereqCaseResult.Status != ResultStatus.Pass) + { + return new TestCaseResult(TestCase.Name, prereqCaseResult.RequestDetails, "Prerequisites failed", prereqCaseResult.Errors, ResultStatus.Skipped); + } + } + + return await ExecuteTestCaseAsync(TestCase); + } + /// /// Executes single TestCase: /// - for each of the WOPI requests defined for that test case @@ -146,7 +161,7 @@ private TestCaseResult ExecuteTestCase(ITestCase testCase) // Save any state that was requested foreach (IStateEntry stateSaver in request.State) { - savedState[stateSaver.Name] = stateSaver.GetValue(responseData); + savedState[ stateSaver.Name ] = stateSaver.GetValue(responseData); } } } @@ -165,6 +180,110 @@ private TestCaseResult ExecuteTestCase(ITestCase testCase) return finalTestResult; } + /// + /// Executes single TestCase: + /// - for each of the WOPI requests defined for that test case + /// --- executes the requests + /// --- runs the validations + /// + private async Task ExecuteTestCaseAsync(ITestCase testCase) + { + IList requestDetails = new List(); + Dictionary savedState = new Dictionary() + { + { Constants.StateOverrides.OriginalAccessToken, AccessToken }, + { Constants.StateOverrides.OriginalWopiSrc, WopiEndpoint }, + }; + + TestCaseResult finalTestResult = null; + + try + { + foreach (IRequest request in testCase.Requests) + { + if (request is DelayRequest delayRequest) + { + await Task.Delay(TimeSpan.FromSeconds((int)delayRequest.DelayTimeInSeconds)); + continue; + } + + IResponseData responseData; + + try + { + responseData = await request.ExecuteAsync(WopiEndpoint, + AccessToken, + AccessTokenTtl, + testCase, + savedState, + ResourceManager, + UserAgent, + ProofKeyProviderNew, + ProofKeyProviderOld); + } + catch (ProofKeySigningException ex) + { + responseData = ExceptionHelper.WrapExceptionInResponseData(ex); + } + + IEnumerable validators = MandatoryValidators.Concat(request.Validators); + List validationResults = validators.Select(validator => validator.Validate(responseData, ResourceManager, savedState)).ToList(); + List validationFailures = validationResults.Where(r => r.HasFailures).ToList(); + + string responseContent = GetResponseContentForClient(responseData); + + RequestInfo requestInfo = new RequestInfo( + request.Name, + request.TargetUrl, + request.RequestHeaders, + responseData.StatusCode, + responseData.Headers, + responseContent, + validationFailures, + responseData.Elapsed, + request.CurrentProofData, + request.OldProofData); + + requestDetails.Add(requestInfo); + + // return on the first request that fails + if (validationFailures.Any()) + { + string testCaseResultMessage = !String.IsNullOrWhiteSpace(testCase.FailMessage) + ? testCase.FailMessage + : string.Format("{0} request failed.", request.Name); + + finalTestResult = new TestCaseResult( + testCase.Name, + requestDetails, + testCaseResultMessage, + validationFailures.SelectMany(x => x.Errors), + ResultStatus.Fail); + break; + } + + // Save any state that was requested + foreach (IStateEntry stateSaver in request.State) + { + savedState[ stateSaver.Name ] = stateSaver.GetValue(responseData); + } + } + } + finally + { + // run the cleanup cases if there were any + await RunCleanupRequestsAsync(testCase, savedState, requestDetails); + } + + if (finalTestResult == null) + { + // if we're here, there were no errors. + finalTestResult = new TestCaseResult(testCase.Name, requestDetails, ResultStatus.Pass); + } + + return finalTestResult; + } + private string GetResponseContentForClient(IResponseData responseData) { string responseContent = "No content"; @@ -263,5 +382,64 @@ private void RunCleanupRequests(ITestCase testCase, Dictionary s } } } + + private async Task RunCleanupRequestsAsync(ITestCase testCase, Dictionary savedState, IList requestDetails) + { + if (testCase.CleanupRequests == null) + return; + + foreach (IRequest request in testCase.CleanupRequests) + { + try + { + IResponseData responseData = await request.ExecuteAsync(WopiEndpoint, + AccessToken, + AccessTokenTtl, + testCase, + savedState, + ResourceManager, + UserAgent, + ProofKeyProviderNew, + ProofKeyProviderOld); + + // No validators needed, they're just cleanup and we don't care if they worked or not. + + string responseContent = GetResponseContentForClient(responseData); + + RequestInfo requestInfo = new RequestInfo( + request.Name, + request.TargetUrl, + request.RequestHeaders, + responseData.StatusCode, + responseData.Headers, + responseContent, + Enumerable.Empty().ToList(), + responseData.Elapsed, + request.CurrentProofData, + request.OldProofData); + + requestDetails.Add(requestInfo); + } + catch (Exception) + { + // Swallow all exceptions from the cleanup requests - we don't care if they work and + // some will fail by design if the test's requests failed. + + RequestInfo requestInfo = new RequestInfo( + request.Name, + request.TargetUrl, + request.RequestHeaders, + 0, + Enumerable.Empty>(), + "no content: request failed", + Enumerable.Empty().ToList(), + TimeSpan.Zero, + request.CurrentProofData, + request.OldProofData); + + requestDetails.Add(requestInfo); + } + } + } } } diff --git a/src/WopiValidator/Options.cs b/src/WopiValidator/Options.cs index 6955368..e454576 100644 --- a/src/WopiValidator/Options.cs +++ b/src/WopiValidator/Options.cs @@ -37,5 +37,8 @@ class Options [Option('d', "include-delaytests", Required = false, HelpText = "Run test cases with delay")] public bool IncludeTestCasesWithDelay { get; set; } + + [Option('a', "run-asynchronously-using-tasks", Required = false, Default = true, HelpText = "(Temporary) Run the tests using the .NET Async/Await APIs instead of the synchronously (defaults to true)")] + public bool RunAsynchronously { get; set; } } } diff --git a/src/WopiValidator/Program.cs b/src/WopiValidator/Program.cs index dcbe5c3..7cb8cb6 100644 --- a/src/WopiValidator/Program.cs +++ b/src/WopiValidator/Program.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Text; +using System.Threading.Tasks; namespace Microsoft.Office.WopiValidator { @@ -34,17 +35,28 @@ private static TestCaseExecutor GetTestCaseExecutor(TestExecutionData testExecut return new TestCaseExecutor(testExecutionData, options.WopiEndpoint, options.AccessToken, options.AccessTokenTtl, userAgent); } - private static int Main(string[] args) + private static async Task Main(string[] args) { // Wrapping all logic in a top-level Exception handler to ensure that exceptions are // logged to the console and don't cause Windows Error Reporting to kick in. ExitCode exitCode = ExitCode.Success; try { - exitCode = Parser.Default.ParseArguments(args) - .MapResult( - (Options options) => Execute(options), - parseErrors => ExitCode.Failure); + bool wasSuccessful = false; + Options options = Parser.Default.ParseArguments(args) + .MapResult(option => + { + wasSuccessful = true; + return option; + }, errors => + { + wasSuccessful = false; + return null; + }); + + exitCode = wasSuccessful + ? await ExecuteAsync(options) + : ExitCode.Failure; } catch (Exception ex) { @@ -60,7 +72,7 @@ private static int Main(string[] args) return (int)exitCode; } - private static ExitCode Execute(Options options) + private static async Task ExecuteAsync(Options options) { // get run configuration from XML IEnumerable testData = ConfigParser.ParseExecutionData(options.RunConfigurationFilePath, options.TestCategory); @@ -107,13 +119,13 @@ private static ExitCode Execute(Options options) continue; } - // define execution query - evaluation is lazy; test cases are executed one at a time - // as you iterate over returned collection - var results = group.Executors.Select(x => x.Execute()); - - // iterate over results and print success/failure indicators into console - foreach (TestCaseResult testCaseResult in results) + // iterate over executors. Compute each result and print success/failure indicators into console + foreach (TestCaseExecutor testCaseExecutor in group.Executors) { + TestCaseResult testCaseResult = options.RunAsynchronously + ? await testCaseExecutor.ExecuteAsync() + : testCaseExecutor.Execute(); + resultStatuses.Add(testCaseResult.Status); switch (testCaseResult.Status) { From 70b3f3a3315240fbfee3e85110d0af889a72f6b7 Mon Sep 17 00:00:00 2001 From: Rob Rolnick Date: Wed, 12 Nov 2025 00:01:30 -0800 Subject: [PATCH 2/4] Restore old behavior. --- src/WopiValidator/Options.cs | 2 +- src/WopiValidator/Program.cs | 139 +++++++++++++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/src/WopiValidator/Options.cs b/src/WopiValidator/Options.cs index e454576..3c7e874 100644 --- a/src/WopiValidator/Options.cs +++ b/src/WopiValidator/Options.cs @@ -38,7 +38,7 @@ class Options [Option('d', "include-delaytests", Required = false, HelpText = "Run test cases with delay")] public bool IncludeTestCasesWithDelay { get; set; } - [Option('a', "run-asynchronously-using-tasks", Required = false, Default = true, HelpText = "(Temporary) Run the tests using the .NET Async/Await APIs instead of the synchronously (defaults to true)")] + [Option('a', "run-asynchronously-using-tasks", Required = false, Default = false, HelpText = "(Temporary) Run the tests using the .NET Async/Await APIs instead of the synchronously (defaults to false)")] public bool RunAsynchronously { get; set; } } } diff --git a/src/WopiValidator/Program.cs b/src/WopiValidator/Program.cs index 7cb8cb6..76cf829 100644 --- a/src/WopiValidator/Program.cs +++ b/src/WopiValidator/Program.cs @@ -54,9 +54,18 @@ private static async Task Main(string[] args) return null; }); - exitCode = wasSuccessful - ? await ExecuteAsync(options) - : ExitCode.Failure; + if (!wasSuccessful) + { + exitCode = ExitCode.Failure; + } + else if (options.RunAsynchronously) + { + exitCode = await ExecuteAsync(options); + } + else + { + exitCode = Execute(options); + } } catch (Exception ex) { @@ -72,6 +81,126 @@ private static async Task Main(string[] args) return (int)exitCode; } + private static ExitCode Execute(Options options) + { + // get run configuration from XML + IEnumerable testData = ConfigParser.ParseExecutionData(options.RunConfigurationFilePath, options.TestCategory); + + if (!String.IsNullOrEmpty(options.TestGroup)) + { + testData = testData.Where(d => d.TestGroupName == options.TestGroup); + } + + IEnumerable executionData; + if (!String.IsNullOrWhiteSpace(options.TestName)) + { + executionData = new TestExecutionData[] { TestExecutionData.GetDataForSpecificTest(testData, options.TestName) }; + } + else + { + executionData = testData; + } + + // Create executor groups + var executorGroups = executionData.GroupBy(d => new + { + d.TestGroupName, + d.TestGroupHasDelay + }) + .Select(g => new + { + Name = g.Key.TestGroupName, + HasDelay = g.Key.TestGroupHasDelay, + Executors = g.Select(x => GetTestCaseExecutor(x, options, options.TestCategory)) + }); + ; + + ConsoleColor baseColor = ConsoleColor.White; + HashSet resultStatuses = new HashSet(); + foreach (var group in executorGroups) + { + WriteToConsole($"\nTest group: {group.Name}\n", ConsoleColor.White); + + // skip test groups using delay + if (group.HasDelay && !options.IncludeTestCasesWithDelay) + { + baseColor = ConsoleColor.Yellow; + WriteToConsole($"All tests skipped: {group.Name} uses delay.\n", baseColor, 1); + continue; + } + + // define execution query - evaluation is lazy; test cases are executed one at a time + // as you iterate over returned collection + var results = group.Executors.Select(x => x.Execute()); + + // iterate over results and print success/failure indicators into console + foreach (TestCaseResult testCaseResult in results) + { + resultStatuses.Add(testCaseResult.Status); + switch (testCaseResult.Status) + { + case ResultStatus.Pass: + baseColor = ConsoleColor.Green; + WriteToConsole($"Pass: {testCaseResult.Name}\n", baseColor, 1); + break; + + case ResultStatus.Skipped: + baseColor = ConsoleColor.Yellow; + if (!options.IgnoreSkipped) + { + WriteToConsole($"Skipped: {testCaseResult.Name}\n", baseColor, 1); + } + break; + + case ResultStatus.Fail: + default: + baseColor = ConsoleColor.Red; + WriteToConsole($"Fail: {testCaseResult.Name}\n", baseColor, 1); + break; + } + + if (testCaseResult.Status == ResultStatus.Fail || + (testCaseResult.Status == ResultStatus.Skipped && !options.IgnoreSkipped)) + { + foreach (var request in testCaseResult.RequestDetails) + { + var responseStatus = (HttpStatusCode)request.ResponseStatusCode; + var color = request.ValidationFailures.Count == 0 ? ConsoleColor.DarkGreen : baseColor; + WriteToConsole($"{request.Name}, response code: {request.ResponseStatusCode} {responseStatus}\n", color, 2); + foreach (var failure in request.ValidationFailures) + { + foreach (var error in failure.Errors) + WriteToConsole($"{error.StripNewLines()}\n", baseColor, 3); + } + } + + WriteToConsole($"Re-run command: .\\wopivalidator.exe -n {testCaseResult.Name} -w {options.WopiEndpoint} -t {options.AccessToken} -l {options.AccessTokenTtl}\n", baseColor, 2); + Console.WriteLine(); + } + } + + if (options.IgnoreSkipped && !resultStatuses.ContainsAny(ResultStatus.Pass, ResultStatus.Fail)) + { + WriteToConsole($"All tests skipped.\n", baseColor, 1); + } + } + + // If skipped tests are ignored, don't consider them when determining whether the test run passed or failed + if (options.IgnoreSkipped) + { + if (resultStatuses.Contains(ResultStatus.Fail)) + { + return ExitCode.Failure; + } + } + // Otherwise consider skipped tests as failures + else if (resultStatuses.ContainsAny(ResultStatus.Skipped, ResultStatus.Fail)) + { + return ExitCode.Failure; + } + return ExitCode.Success; + } + private static async Task ExecuteAsync(Options options) { // get run configuration from XML @@ -122,9 +251,7 @@ private static async Task ExecuteAsync(Options options) // iterate over executors. Compute each result and print success/failure indicators into console foreach (TestCaseExecutor testCaseExecutor in group.Executors) { - TestCaseResult testCaseResult = options.RunAsynchronously - ? await testCaseExecutor.ExecuteAsync() - : testCaseExecutor.Execute(); + TestCaseResult testCaseResult = await testCaseExecutor.ExecuteAsync(); resultStatuses.Add(testCaseResult.Status); switch (testCaseResult.Status) From 68fcd48484d038f8d54c1bf5788834aa9fb567f0 Mon Sep 17 00:00:00 2001 From: Rob Rolnick Date: Wed, 12 Nov 2025 00:06:58 -0800 Subject: [PATCH 3/4] Cleanup --- src/WopiValidator/Program.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/WopiValidator/Program.cs b/src/WopiValidator/Program.cs index 76cf829..95c7571 100644 --- a/src/WopiValidator/Program.cs +++ b/src/WopiValidator/Program.cs @@ -103,17 +103,16 @@ private static ExitCode Execute(Options options) // Create executor groups var executorGroups = executionData.GroupBy(d => new - { - d.TestGroupName, - d.TestGroupHasDelay - }) + { + d.TestGroupName, + d.TestGroupHasDelay + }) .Select(g => new { Name = g.Key.TestGroupName, HasDelay = g.Key.TestGroupHasDelay, Executors = g.Select(x => GetTestCaseExecutor(x, options, options.TestCategory)) }); - ; ConsoleColor baseColor = ConsoleColor.White; HashSet resultStatuses = new HashSet(); From 5882ed7fe612bbd52b88e2671ac17cf26e137ddb Mon Sep 17 00:00:00 2001 From: Rob Rolnick Date: Wed, 12 Nov 2025 20:36:17 -0800 Subject: [PATCH 4/4] Mark await calls with ConfigureAwait(false). --- .../Requests/GetFromFileUrlRequest.cs | 4 ++-- src/WopiValidator.Core/Requests/RequestBase.cs | 12 ++++++------ src/WopiValidator.Core/Requests/WopiRequest.cs | 4 ++-- src/WopiValidator.Core/TestCaseExecutor.cs | 12 ++++++------ src/WopiValidator/Program.cs | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs b/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs index 9e1f4d1..b985fa5 100644 --- a/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs +++ b/src/WopiValidator.Core/Requests/GetFromFileUrlRequest.cs @@ -45,7 +45,7 @@ public override IResponseData Execute( return ExecuteRequest(executionData); } - public override async Task ExecuteAsync( + public async override Task ExecuteAsync( string endpointAddress, string accessToken, long accessTokenTtl, @@ -61,7 +61,7 @@ public override async Task ExecuteAsync( Enumerable.Empty>(), null); - return await ExecuteRequestAsync(executionData); + return await ExecuteRequestAsync(executionData).ConfigureAwait(false); } } } diff --git a/src/WopiValidator.Core/Requests/RequestBase.cs b/src/WopiValidator.Core/Requests/RequestBase.cs index 5901f86..e8093ef 100644 --- a/src/WopiValidator.Core/Requests/RequestBase.cs +++ b/src/WopiValidator.Core/Requests/RequestBase.cs @@ -131,10 +131,10 @@ protected async Task ExecuteRequestAsync( if (content != null) { request.ContentLength = content.Length; - using (Stream requestStream = await request.GetRequestStreamAsync()) + using (Stream requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false)) { content.Seek(0, SeekOrigin.Begin); - await content.CopyToAsync(requestStream); + await content.CopyToAsync(requestStream).ConfigureAwait(false); } } else @@ -145,10 +145,10 @@ protected async Task ExecuteRequestAsync( try { timer.Start(); - using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync()) + using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false)) { timer.Stop(); - return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed); + return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed).ConfigureAwait(false); } } // ProtocolErrors will have a non-null Response object so we can still get response details @@ -157,7 +157,7 @@ protected async Task ExecuteRequestAsync( using (HttpWebResponse response = (HttpWebResponse)ex.Response) { timer.Stop(); - return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed); + return await GetResponseDataAsync(response, IsTextResponseExpected, timer.Elapsed).ConfigureAwait(false); } } // no response, so we wrap the exception details so they can be included in a validation failure @@ -250,7 +250,7 @@ private static async Task GetResponseDataAsync(HttpWebResponse re using (Stream responseStream = response.GetResponseStream()) { if (responseStream != null) - await responseStream.CopyToAsync(content); + await responseStream.CopyToAsync(content).ConfigureAwait(false); } // just to be sure diff --git a/src/WopiValidator.Core/Requests/WopiRequest.cs b/src/WopiValidator.Core/Requests/WopiRequest.cs index 23f4cf3..f8aa324 100644 --- a/src/WopiValidator.Core/Requests/WopiRequest.cs +++ b/src/WopiValidator.Core/Requests/WopiRequest.cs @@ -79,7 +79,7 @@ public override IResponseData Execute(string endpointAddress, /// /// Executes WOPI request at given WOPI endpoint address against provided wopi FileRep. /// - public override async Task ExecuteAsync(string endpointAddress, + public async override Task ExecuteAsync(string endpointAddress, string accessToken, long accessTokenTtl, ITestCase testCase, @@ -90,7 +90,7 @@ public override async Task ExecuteAsync(string endpointAddress, RSACryptoServiceProvider proofKeyProviderOld) { RequestExecutionData executionData = CreateExecutionData(endpointAddress, ref accessToken, accessTokenTtl, savedState, resourceManager, proofKeyProviderNew, proofKeyProviderOld); - return await ExecuteRequestAsync(executionData, userAgent); + return await ExecuteRequestAsync(executionData, userAgent).ConfigureAwait(false); } private RequestExecutionData CreateExecutionData(string endpointAddress, ref string accessToken, long accessTokenTtl, Dictionary savedState, IResourceManager resourceManager, RSACryptoServiceProvider proofKeyProviderNew, RSACryptoServiceProvider proofKeyProviderOld) diff --git a/src/WopiValidator.Core/TestCaseExecutor.cs b/src/WopiValidator.Core/TestCaseExecutor.cs index 406232f..63849d4 100644 --- a/src/WopiValidator.Core/TestCaseExecutor.cs +++ b/src/WopiValidator.Core/TestCaseExecutor.cs @@ -66,14 +66,14 @@ public async Task ExecuteAsync() { foreach (var prereqCase in PrereqCases) { - TestCaseResult prereqCaseResult = await ExecuteTestCaseAsync(prereqCase); + TestCaseResult prereqCaseResult = await ExecuteTestCaseAsync(prereqCase).ConfigureAwait(false); if (prereqCaseResult.Status != ResultStatus.Pass) { return new TestCaseResult(TestCase.Name, prereqCaseResult.RequestDetails, "Prerequisites failed", prereqCaseResult.Errors, ResultStatus.Skipped); } } - return await ExecuteTestCaseAsync(TestCase); + return await ExecuteTestCaseAsync(TestCase).ConfigureAwait(false); } /// @@ -203,7 +203,7 @@ private async Task ExecuteTestCaseAsync(ITestCase testCase) { if (request is DelayRequest delayRequest) { - await Task.Delay(TimeSpan.FromSeconds((int)delayRequest.DelayTimeInSeconds)); + await Task.Delay(TimeSpan.FromSeconds((int)delayRequest.DelayTimeInSeconds)).ConfigureAwait(false); continue; } @@ -219,7 +219,7 @@ private async Task ExecuteTestCaseAsync(ITestCase testCase) ResourceManager, UserAgent, ProofKeyProviderNew, - ProofKeyProviderOld); + ProofKeyProviderOld).ConfigureAwait(false); } catch (ProofKeySigningException ex) { @@ -272,7 +272,7 @@ private async Task ExecuteTestCaseAsync(ITestCase testCase) finally { // run the cleanup cases if there were any - await RunCleanupRequestsAsync(testCase, savedState, requestDetails); + await RunCleanupRequestsAsync(testCase, savedState, requestDetails).ConfigureAwait(false); } if (finalTestResult == null) @@ -400,7 +400,7 @@ private async Task RunCleanupRequestsAsync(ITestCase testCase, Dictionary Main(string[] args) } else if (options.RunAsynchronously) { - exitCode = await ExecuteAsync(options); + exitCode = await ExecuteAsync(options).ConfigureAwait(false); } else { @@ -250,7 +250,7 @@ private static async Task ExecuteAsync(Options options) // iterate over executors. Compute each result and print success/failure indicators into console foreach (TestCaseExecutor testCaseExecutor in group.Executors) { - TestCaseResult testCaseResult = await testCaseExecutor.ExecuteAsync(); + TestCaseResult testCaseResult = await testCaseExecutor.ExecuteAsync().ConfigureAwait(false); resultStatuses.Add(testCaseResult.Status); switch (testCaseResult.Status)