From 98016a760e0b74d0cfee318794aad4ed4455f72d Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Mon, 25 May 2020 10:00:57 -0400 Subject: [PATCH] [Harness] Move the periodic command execution logic out of Jenkins. (#8657) Move the logic out, Jenkins class should just orchestrate all the diff tasks but should not know how they are executed. --- tests/xharness/Jenkins/Jenkins.cs | 29 ++---- tests/xharness/Jenkins/PeriodicCommand.cs | 61 ++++++++++++ .../Jenkins/PeriodicCommandTests.cs | 92 +++++++++++++++++++ .../Xharness.Tests/Xharness.Tests.csproj | 1 + tests/xharness/xharness.csproj | 1 + 5 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 tests/xharness/Jenkins/PeriodicCommand.cs create mode 100644 tests/xharness/Xharness.Tests/Jenkins/PeriodicCommandTests.cs diff --git a/tests/xharness/Jenkins/Jenkins.cs b/tests/xharness/Jenkins/Jenkins.cs index 9eeadf05ab..70b194efb7 100644 --- a/tests/xharness/Jenkins/Jenkins.cs +++ b/tests/xharness/Jenkins/Jenkins.cs @@ -517,26 +517,6 @@ namespace Xharness.Jenkins { return Task.WhenAll (loadsim, loaddev); } - async Task ExecutePeriodicCommandAsync (ILog periodic_loc) - { - periodic_loc.WriteLine ($"Starting periodic task with interval {Harness.PeriodicCommandInterval.TotalMinutes} minutes."); - while (true) { - var watch = Stopwatch.StartNew (); - using (var process = new Process ()) { - process.StartInfo.FileName = Harness.PeriodicCommand; - process.StartInfo.Arguments = Harness.PeriodicCommandArguments; - var rv = await processManager.RunAsync (process, periodic_loc, timeout: Harness.PeriodicCommandInterval); - if (!rv.Succeeded) - periodic_loc.WriteLine ($"Periodic command failed with exit code {rv.ExitCode} (Timed out: {rv.TimedOut})"); - } - var ticksLeft = watch.ElapsedTicks - Harness.PeriodicCommandInterval.Ticks; - if (ticksLeft < 0) - ticksLeft = Harness.PeriodicCommandInterval.Ticks; - var wait = TimeSpan.FromTicks (ticksLeft); - await Task.Delay (wait); - } - } - public int Run () { try { @@ -559,8 +539,13 @@ namespace Xharness.Jenkins { }); } if (!string.IsNullOrEmpty (Harness.PeriodicCommand)) { - var periodic_log = Logs.Create ("PeriodicCommand.log", "Periodic command log"); - Task.Run (async () => await ExecutePeriodicCommandAsync (periodic_log)); + var periodicCommand = new PeriodicCommand ( + command: Harness.PeriodicCommand, + processManager: processManager, + interval: Harness.PeriodicCommandInterval, + logs: logs, + arguments: string.IsNullOrEmpty (Harness.PeriodicCommandArguments) ? null : Harness.PeriodicCommandArguments); + periodicCommand.Execute ().DoNotAwait (); } // We can populate and build test-libraries in parallel. diff --git a/tests/xharness/Jenkins/PeriodicCommand.cs b/tests/xharness/Jenkins/PeriodicCommand.cs new file mode 100644 index 0000000000..60c0dd38b9 --- /dev/null +++ b/tests/xharness/Jenkins/PeriodicCommand.cs @@ -0,0 +1,61 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.XHarness.iOS.Shared.Execution; +using Microsoft.DotNet.XHarness.iOS.Shared.Logging; + +#nullable enable +namespace Xharness.Jenkins { + + /// + /// Represent a command that will be executed periodically. + /// + class PeriodicCommand { + + readonly string command; + readonly string? arguments; + readonly TimeSpan interval; + readonly IProcessManager processManager; + readonly ILog log; + + public PeriodicCommand (string command, IProcessManager processManager, TimeSpan interval, ILogs logs, string? arguments = null) + { + if (logs == null) + throw new ArgumentNullException (nameof (logs)); + + this.log = logs.Create ("PeriodicCommand.log", "Periodic command log"); + this.command = command ?? throw new ArgumentNullException (nameof (command)); + this.processManager = processManager ?? throw new ArgumentNullException (nameof (processManager)); + this.interval = interval; + this.arguments = arguments; + } + + async Task ExecuteInternal (CancellationToken? cancellationToken = null) + { + log.WriteLine ($"Starting periodic task with interval {interval.TotalMinutes} minutes."); + while (true) { + var watch = Stopwatch.StartNew (); + using (var process = new Process ()) { + process.StartInfo.FileName = command; + process.StartInfo.Arguments = arguments; + ProcessExecutionResult? rv = cancellationToken.HasValue + ? await processManager.RunAsync (process, log, timeout: interval, cancellation_token: cancellationToken) + : await processManager.RunAsync (process, log, timeout: interval); + if (rv != null && !rv.Succeeded) + log.WriteLine ($"Periodic command failed with exit code {rv.ExitCode} (Timed out: {rv.TimedOut})"); + } + var ticksLeft = watch.ElapsedTicks - interval.Ticks; + if (ticksLeft < 0) + ticksLeft = interval.Ticks; + var wait = TimeSpan.FromTicks (ticksLeft); + await Task.Delay (wait); + } + } + + public Task Execute (CancellationToken? cancellationToken = null) + => cancellationToken != null + ? Task.Run (async () => await ExecuteInternal (cancellationToken.Value), cancellationToken.Value) + : Task.Run (async () => await ExecuteInternal ()); + } +} diff --git a/tests/xharness/Xharness.Tests/Jenkins/PeriodicCommandTests.cs b/tests/xharness/Xharness.Tests/Jenkins/PeriodicCommandTests.cs new file mode 100644 index 0000000000..e49717fbca --- /dev/null +++ b/tests/xharness/Xharness.Tests/Jenkins/PeriodicCommandTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.XHarness.iOS.Shared.Execution; +using Microsoft.DotNet.XHarness.iOS.Shared.Logging; +using Moq; +using NUnit.Framework; +using Xharness.Jenkins; + +namespace Xharness.Tests.Jenkins { + + [TestFixture] + public class PeriodicCommandTests { + + Mock processManager; + Mock logs; + Mock log; + TimeSpan interval; + string command; + string arguments; + + [SetUp] + public void SetUp () + { + processManager = new Mock (MockBehavior.Strict); + logs = new Mock (); + log = new Mock (); + + // common setup for the mocks + logs.Setup (l => l.Create (It.Is (s => true), It.Is (s => true), null)).Returns (log.Object); + interval = TimeSpan.FromMilliseconds (100); + command = "test"; + arguments = "periodic"; + } + + // we do not test the options without the cancellation task because we want to be nice people when running + // the tests and do not leave a thread doing nothing + [Test] + public async Task TestExecuteNoArgs () + { + var periodicCommand = new PeriodicCommand (command, processManager.Object, interval, logs.Object); + var executionTcs = new TaskCompletionSource (); + var threadCs = new CancellationTokenSource (); + + processManager.Setup (pm => pm.RunAsync ( + It.Is (p => p.StartInfo.FileName == command && p.StartInfo.Arguments == string.Empty), + It.IsAny (), + interval, + null, + It.IsAny (), + null)).Callback, CancellationToken?, bool?> ( + (p, l, i, env, t, d) => { + executionTcs.TrySetResult (true); + }).ReturnsAsync (new ProcessExecutionResult { ExitCode = 0, TimedOut = false }).Verifiable (); + + var task = periodicCommand.Execute (threadCs.Token); + await executionTcs.Task; // wait for the callback in the mock, which is in another thread to set the source + processManager.VerifyAll (); + processManager.VerifyNoOtherCalls (); + threadCs.Cancel (); // clean + } + + [Test] + public async Task TestExecuteArgs () + { + // all similar logic to the above one, but with arguments + var periodicCommand = new PeriodicCommand (command, processManager.Object, interval, logs.Object, arguments: arguments); + var executionTcs = new TaskCompletionSource (); + var threadCs = new CancellationTokenSource (); + + processManager.Setup (pm => pm.RunAsync ( + It.Is (p => p.StartInfo.FileName == command && p.StartInfo.Arguments == arguments), + It.IsAny (), + interval, + null, + It.IsAny (), + null)).Callback, CancellationToken?, bool?> ( + (p, l, i, env, t, d) => { + executionTcs.TrySetResult (true); + }).ReturnsAsync (new ProcessExecutionResult { ExitCode = 0, TimedOut = false }).Verifiable (); + + var task = periodicCommand.Execute (threadCs.Token); + await executionTcs.Task; // wait for the callback in the mock, which is in another thread to set the source + processManager.VerifyAll (); + processManager.VerifyNoOtherCalls (); + threadCs.Cancel (); // clean + } + + } +} diff --git a/tests/xharness/Xharness.Tests/Xharness.Tests.csproj b/tests/xharness/Xharness.Tests/Xharness.Tests.csproj index f3048cd0b8..020d2a96d8 100644 --- a/tests/xharness/Xharness.Tests/Xharness.Tests.csproj +++ b/tests/xharness/Xharness.Tests/Xharness.Tests.csproj @@ -68,6 +68,7 @@ + diff --git a/tests/xharness/xharness.csproj b/tests/xharness/xharness.csproj index 05616a02f1..cbacd79e88 100644 --- a/tests/xharness/xharness.csproj +++ b/tests/xharness/xharness.csproj @@ -136,6 +136,7 @@ +