| File: Execution\TestRunCache.cs | Web Access |
| Project: src\vstest\src\Microsoft.TestPlatform.CrossPlatEngine\Microsoft.TestPlatform.CrossPlatEngine.csproj (Microsoft.TestPlatform.CrossPlatEngine) |
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Execution; /// <summary> /// Maintains a cache of last 'n' test results and maintains stats for the complete run. /// </summary> internal class TestRunCache : ITestRunCache { /// <summary> /// Specifies whether the object is disposed or not. /// </summary> private bool _isDisposed; /// <summary> /// Test run stats /// </summary> private readonly Dictionary<TestOutcome, long> _runStats; /// <summary> /// Total tests which have currently executed /// </summary> private long _totalExecutedTests; /// <summary> /// Callback used when cache is ready to report some test results/case. /// </summary> private readonly OnCacheHit _onCacheHit; /// <summary> /// Max size of the test result buffer /// </summary> private readonly long _cacheSize; /// <summary> /// Timeout that triggers sending results regardless of cache size. /// </summary> private readonly TimeSpan _cacheTimeout; /// <summary> /// Timer for cache /// </summary> private readonly Timer _timer; /// <summary> /// Last time results were sent. /// </summary> private DateTime _lastUpdate; /// <summary> /// The test case currently in progress. /// </summary> private List<TestCase> _inProgressTests; /// <summary> /// Test results buffer /// </summary> private List<TestResult> _testResults; /// <summary> /// Sync object /// </summary> private readonly object _syncObject; /// <summary> /// Initializes a new instance of the <see cref="TestRunCache"/> class. /// </summary> /// <param name="cacheSize"> The cache size. </param> /// <param name="cacheTimeout"> The cache timeout. </param> /// <param name="onCacheHit"> The on cache hit. </param> internal TestRunCache(long cacheSize, TimeSpan cacheTimeout, OnCacheHit onCacheHit) { TPDebug.Assert(cacheSize > 0, "Buffer size cannot be less than zero"); TPDebug.Assert(onCacheHit != null, "Callback which listens for cache size limit cannot be null."); TPDebug.Assert(cacheTimeout > TimeSpan.MinValue, "The cache timeout must be greater than min value."); if (cacheTimeout.TotalMilliseconds > int.MaxValue) { cacheTimeout = TimeSpan.FromMilliseconds(int.MaxValue / 2); } _cacheSize = cacheSize; _onCacheHit = onCacheHit; _lastUpdate = DateTime.UtcNow; _cacheTimeout = cacheTimeout; _inProgressTests = new List<TestCase>(InitialCapacity(cacheSize)); _testResults = new List<TestResult>(InitialCapacity(cacheSize)); _runStats = new Dictionary<TestOutcome, long>(); _syncObject = new object(); _timer = new Timer(OnCacheTimeHit, this, cacheTimeout, cacheTimeout); } /// <summary> /// Called when the cache is ready to report on the current status. /// </summary> internal delegate void OnCacheHit(TestRunStatistics testRunStats, ICollection<TestResult> results, ICollection<TestCase> inProgressTests); /// <summary> /// Gets the test results present in the cache currently. /// </summary> public ICollection<TestResult> TestResults { get { lock (_syncObject) { return _testResults; } } } /// <summary> /// Gets the set of in-progress test cases present in the cache currently. /// </summary> public ICollection<TestCase> InProgressTests { get { lock (_syncObject) { return _inProgressTests; } } } /// <summary> /// Gets the total executed tests. /// </summary> public long TotalExecutedTests { get { lock (_syncObject) { return _totalExecutedTests; } } } /// <summary> /// Gets the test run stats /// </summary> public TestRunStatistics TestRunStatistics { get { lock (_syncObject) { var stats = new TestRunStatistics(new Dictionary<TestOutcome, long>(_runStats)); stats.ExecutedTests = TotalExecutedTests; return stats; } } } public IDictionary<string, int> AdapterTelemetry { get; set; } = new Dictionary<string, int>(); /// <summary> /// Disposes the cache /// </summary> public void Dispose() { Dispose(true); // Use SupressFinalize in case a subclass // of this valueType implements a finalizer. GC.SuppressFinalize(this); } /// <summary> /// Notifies the cache that a test is starting. /// This notification comes from the adapter to the engine which then calls into the cache. /// </summary> /// <param name="testCase"> The test Case. </param> public void OnTestStarted(TestCase testCase) { lock (_syncObject) { _inProgressTests.Add(testCase); CheckForCacheHit(); } } /// <summary> /// Notifies the cache of a new test result from the adapter. /// </summary> /// <param name="testResult"> The test result. </param> public void OnNewTestResult(TestResult testResult) { lock (_syncObject) { _totalExecutedTests++; _testResults.Add(testResult); MsTestV1TelemetryHelper.AddTelemetry(testResult, AdapterTelemetry); if (_runStats.TryGetValue(testResult.Outcome, out long count)) { count++; } else { count = 1; } _runStats[testResult.Outcome] = count; RemoveInProgress(testResult); CheckForCacheHit(); } } /// <summary> /// Notifies the cache of a test completion. /// </summary> /// <param name="completedTest"> /// The completed Test. /// </param> /// <returns> True if this test has been removed from the list of in progress tests. </returns> public bool OnTestCompletion(TestCase? completedTest) { lock (_syncObject) { if (completedTest == null) { EqtTrace.Warning("TestRunCache: completedTest is null"); return false; } if (_inProgressTests == null || _inProgressTests.Count == 0) { EqtTrace.Warning("TestRunCache: InProgressTests is null"); return false; } var removed = _inProgressTests.Remove(completedTest); if (removed) { return true; } // Try finding/removing a matching test corresponding to the completed test var inProgressTest = _inProgressTests.FirstOrDefault(inProgress => inProgress.Id == completedTest.Id); if (inProgressTest != null) { removed = _inProgressTests.Remove(inProgressTest); } return removed; } } /// <summary> /// Returns the last chunk /// </summary> /// <returns> The set of test results remaining in the cache. </returns> public ICollection<TestResult> GetLastChunk() { lock (_syncObject) { var lastChunk = _testResults; // GetLastChunk() is the end-of-run drain; no further results are expected after this // call, so avoid pre-allocating capacity here. The replacement list exists only to keep // the field non-null and will be allocated lazily if results somehow arrive afterwards. _testResults = new List<TestResult>(); return lastChunk; } } /// <summary> /// The dispose. /// </summary> /// <param name="disposing"> Indicates if this needs to clean up managed resources. </param> /// <remarks> /// The dispose pattern is a best practice to differentiate between managed and native resources cleanup. /// Even though this particular class does not have any native resources - honoring the pattern to be consistent throughout the code base. /// </remarks> protected virtual void Dispose(bool disposing) { // If you need thread safety, use a lock around these // operations, as well as in your methods that use the resource. if (disposing && !_isDisposed) { if (_timer != null) { _timer.Dispose(); } // Indicate that the instance has been disposed. _isDisposed = true; } } /// <summary> /// Checks if the cache timeout/size has been met. /// </summary> private void CheckForCacheHit() { lock (_syncObject) { // Send results when the specified cache size has been reached or // after the specified cache timeout has been hit. var timeDelta = DateTime.UtcNow - _lastUpdate; var inProgressTestsCount = _inProgressTests.Count; if ((_testResults.Count + inProgressTestsCount) >= _cacheSize || (timeDelta >= _cacheTimeout && inProgressTestsCount > 0)) { SendResults(); } } } private void CheckForCacheHitOnTimer() { lock (_syncObject) { if (_testResults.Count > 0 || _inProgressTests.Count > 0) { SendResults(); } } } private void SendResults() { // Pass on the buffer to the listener and clear the old one _onCacheHit(TestRunStatistics, _testResults, _inProgressTests); _testResults = new List<TestResult>(InitialCapacity(_cacheSize)); _inProgressTests = new List<TestCase>(InitialCapacity(_cacheSize)); _lastUpdate = DateTime.UtcNow; // Reset the timer _timer.Change(_cacheTimeout, _cacheTimeout); EqtTrace.Verbose("TestRunCache: OnNewTestResult: Notified the onCacheHit callback."); } private void OnCacheTimeHit(object? state) { lock (_syncObject) { try { CheckForCacheHitOnTimer(); } catch (Exception ex) { EqtTrace.Error("TestRunCache: OnCacheTimeHit: Exception occurred while checking for cache hit. {0}", ex); } } } private void RemoveInProgress(TestResult result) { var removed = OnTestCompletion(result.TestCase); if (!removed) { EqtTrace.Warning("TestRunCache: No test found corresponding to testResult '{0}' in inProgress list.", result.DisplayName); } } private static int InitialCapacity(long cacheSize) => (int)Math.Min(cacheSize, 512); }