Skip to content

Commit 408376b

Browse files
authored
IgnoredFailures Status and Fix Engine Cancellation Token (#99)
* Fix Cancellation Token being cancelled when Ignore Failure is true * IgnoredFailure Status * Update ProgressPrinter.cs * ReleaseNotes.md * Failure tests
1 parent 58c4bee commit 408376b

File tree

12 files changed

+142
-37
lines changed

12 files changed

+142
-37
lines changed

src/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
namespace ModularPipelines.Build.Modules.LocalMachine;
99

10-
[DependsOn<RunUnitTestsModule>]
11-
[DependsOn<PackagePathsParserModule>]
1210
public class CreateLocalNugetFolderModule : Module<Folder>
1311
{
1412
protected override async Task<ModuleResult<Folder>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)

src/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ protected override async Task<bool> ShouldSkip(IModuleContext context)
6767
.UploadPackages(new NuGetUploadOptions(packagePaths.Value!.AsPaths(), new Uri("https://api.nuget.org/v3/index.json"))
6868
{
6969
ApiKey = _nugetSettings.Value.ApiKey!,
70-
NoSymbols = true
7170
});
7271
}
7372
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
- README and License added to NuGet packages
1+
- README and License added to NuGet packages
2+
- Fix issue with the pipeline terminating even if IgnoreFailure was true on a module
3+
- Added an IgnoredFailure status type for modules

src/ModularPipelines/Enums/Status.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public enum Status
66
Processing,
77
Successful,
88
Failed,
9+
IgnoredFailure,
910
PipelineTerminated,
1011
TimedOut,
1112
Skipped,
Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
1-
using System.Runtime.Serialization;
1+
using ModularPipelines.Modules;
22

33
namespace ModularPipelines.Exceptions;
44

55
public class ModuleFailedException : PipelineException
66
{
7-
public ModuleFailedException()
8-
{
9-
}
10-
11-
protected ModuleFailedException(SerializationInfo info, StreamingContext context) : base(info, context)
12-
{
13-
}
14-
15-
public ModuleFailedException(string? message) : base(message)
16-
{
17-
}
7+
public ModuleBase Module { get; }
188

19-
public ModuleFailedException(string? message, Exception? innerException) : base(message, innerException)
9+
public ModuleFailedException(ModuleBase module, Exception exception) : base($"The module {module.GetType().Name} has failed", exception)
2010
{
11+
Module = module;
2112
}
2213
}

src/ModularPipelines/Extensions/HostExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ public static async Task<IReadOnlyList<ModuleBase>> ExecuteAsync(this IHost host
1818
}
1919
finally
2020
{
21-
await ((ServiceProvider) host.Services).DisposeAsync();
21+
if (!IsRunningFromNUnit)
22+
{
23+
await ((ServiceProvider) host.Services).DisposeAsync();
24+
}
2225
}
2326
}
27+
28+
private static readonly bool IsRunningFromNUnit =
29+
AppDomain.CurrentDomain.GetAssemblies().Any(
30+
a => a.FullName?.ToLowerInvariant().StartsWith("nunit.framework") == true);
2431
}

src/ModularPipelines/Helpers/ProgressPrinter.cs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModularPipelines.Modules;
44
using ModularPipelines.Options;
55
using Spectre.Console;
6+
using Status = ModularPipelines.Enums.Status;
67

78
namespace ModularPipelines.Helpers;
89

@@ -118,29 +119,58 @@ private static void RegisterModules(IReadOnlyList<RunnableModule> modulesToProce
118119
progressTask.Increment(ticksPerSecond);
119120
}
120121
}, cancellationToken);
122+
123+
SetupSkippedCallback(cancellationToken, moduleToProcess, progressTask, moduleName);
124+
SetupFinishedSuccessfullyCallback(modulesToProcess, totalTask, cancellationToken, moduleToProcess, progressTask, moduleName);
125+
126+
RegisterSubModules(moduleToProcess, progressContext, cancellationToken);
127+
}
128+
}
121129

122-
// Callback for Module has finished
123-
_ = moduleToProcess.Module.ResultTaskInternal.ContinueWith(t =>
130+
private static void SetupSkippedCallback(CancellationToken cancellationToken, RunnableModule moduleToProcess,
131+
ProgressTask progressTask, string moduleName)
132+
{
133+
// Callback for Module has been ignored
134+
_ = moduleToProcess.Module.SkippedTask.ContinueWith(t =>
135+
{
136+
lock (moduleToProcess)
137+
{
138+
progressTask.Description = $"[yellow][[Skipped]] {moduleName}[/]";
139+
progressTask.StopTask();
140+
}
141+
}, cancellationToken);
142+
}
143+
144+
private static void SetupFinishedSuccessfullyCallback(IReadOnlyList<RunnableModule> modulesToProcess, ProgressTask totalTask,
145+
CancellationToken cancellationToken, RunnableModule moduleToProcess, ProgressTask progressTask, string moduleName)
146+
{
147+
// Callback for Module has finished
148+
_ = moduleToProcess.Module.ResultTaskInternal.ContinueWith(t =>
149+
{
150+
lock (moduleToProcess)
124151
{
152+
if (progressTask.IsFinished)
153+
{
154+
return;
155+
}
156+
125157
if (t.IsCompletedSuccessfully)
126158
{
127159
progressTask.Increment(100);
128160
}
129161

130-
progressTask.Description = t.IsCompletedSuccessfully ? $"[green]{moduleName}[/]" : $"[red][[Failed]] {moduleName}[/]";
162+
progressTask.Description =
163+
t.IsCompletedSuccessfully ? $"{GetColour()}{moduleName}[/]" : $"[red][[Failed]] {moduleName}[/]";
131164

132165
progressTask.StopTask();
133-
totalTask.Increment(100.0 / modulesToProcess.Count);
134-
}, cancellationToken);
135166

136-
// Callback for Module has been ignored
137-
_ = moduleToProcess.Module.IgnoreTask.ContinueWith(t =>
138-
{
139-
progressTask.Description = $"[yellow][[Ignored]] {moduleName}[/]";
140-
progressTask.StopTask();
141-
}, cancellationToken);
142-
143-
RegisterSubModules(moduleToProcess, progressContext, cancellationToken);
167+
totalTask.Increment(100.0 / modulesToProcess.Count);
168+
}
169+
}, cancellationToken);
170+
171+
string GetColour()
172+
{
173+
return moduleToProcess.Module.Status == Status.Successful ? "[green]" : "[orange3]";
144174
}
145175
}
146176

src/ModularPipelines/Modules/Module.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,25 +160,27 @@ internal override async Task StartAsync()
160160

161161
Exception = exception;
162162

163-
Context.EngineCancellationToken.Cancel();
164-
165163
if (await ShouldIgnoreFailures(Context, exception))
166164
{
167165
var moduleResult = ModuleResult.FromException<T>(exception);
168166
moduleResult.ModuleName = GetType().Name;
169167

168+
Status = Status.IgnoredFailure;
169+
170170
await Context.ModuleResultRepository.SaveResultAsync(this, moduleResult);
171171

172172
TaskCompletionSource.SetResult(moduleResult);
173173
}
174174
else
175175
{
176+
Context.EngineCancellationToken.Cancel();
177+
176178
// Give time for Engine to request cancellation
177179
await Task.Delay(300);
178180

179181
TaskCompletionSource.SetException(exception);
180182

181-
throw;
183+
throw new ModuleFailedException(this, exception);
182184
}
183185
}
184186
finally
@@ -315,7 +317,7 @@ internal override void SetSkipped()
315317
{
316318
Status = Status.Skipped;
317319

318-
IgnoreTask.Start(TaskScheduler.Default);
320+
SkippedTask.Start(TaskScheduler.Default);
319321

320322
TaskCompletionSource.SetResult(new SkippedModuleResult<T>());
321323

src/ModularPipelines/Modules/ModuleBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public abstract class ModuleBase
1212
protected internal IModuleContext Context = null!; // Late Initialisation
1313

1414
internal readonly Task StartTask = new(() => { });
15-
internal readonly Task IgnoreTask = new(() => { });
15+
internal readonly Task SkippedTask = new(() => { });
1616
internal abstract Task<object> ResultTaskInternal { get; }
1717

1818
internal readonly CancellationTokenSource ModuleCancellationTokenSource = new();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularPipelines.Context;
3+
using ModularPipelines.Engine;
4+
using ModularPipelines.Models;
5+
using ModularPipelines.Modules;
6+
7+
namespace ModularPipelines.UnitTests;
8+
9+
public class IgnoredFailureTests : TestBase
10+
{
11+
private class IgnoredFailureModule : Module<CommandResult>
12+
{
13+
protected override Task<bool> ShouldIgnoreFailures(IModuleContext context, Exception exception)
14+
{
15+
return Task.FromResult(true);
16+
}
17+
18+
protected override async Task<ModuleResult<CommandResult>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
19+
{
20+
await Task.Yield();
21+
throw new Exception();
22+
}
23+
}
24+
25+
[Test]
26+
public async Task Has_Not_Thrown_Or_Cancelled_Pipeline()
27+
{
28+
var module = await RunModule<IgnoredFailureModule>();
29+
30+
var serviceProvider = module.Context.Get<IServiceProvider>()!;
31+
var engineCancellationToken = serviceProvider.GetRequiredService<EngineCancellationToken>();
32+
33+
var moduleResult = await module;
34+
35+
Assert.Multiple(() =>
36+
{
37+
Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.Failure));
38+
Assert.That(moduleResult.Exception, Is.Not.Null);
39+
Assert.That(engineCancellationToken.IsCancellationRequested, Is.False);
40+
});
41+
}
42+
}

test/ModularPipelines.UnitTests/ModuleTimeoutTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModularPipelines.Context;
2+
using ModularPipelines.Exceptions;
23
using ModularPipelines.Models;
34
using ModularPipelines.Modules;
45

@@ -20,6 +21,7 @@ private class Module : Module<string>
2021
[Test]
2122
public void Throws_Timeout_Exception()
2223
{
23-
Assert.ThrowsAsync<TaskCanceledException>(RunModule<Module>);
24+
var exception = Assert.ThrowsAsync<ModuleFailedException>(RunModule<Module>);
25+
Assert.That(exception!.InnerException, Is.TypeOf<TaskCanceledException>());
2426
}
2527
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularPipelines.Context;
3+
using ModularPipelines.Engine;
4+
using ModularPipelines.Exceptions;
5+
using ModularPipelines.Models;
6+
using ModularPipelines.Modules;
7+
8+
namespace ModularPipelines.UnitTests;
9+
10+
public class NonIgnoredFailureTests : TestBase
11+
{
12+
private class NonIgnoredFailureModule : Module<CommandResult>
13+
{
14+
protected override async Task<ModuleResult<CommandResult>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
15+
{
16+
await Task.Yield();
17+
throw new Exception();
18+
}
19+
}
20+
21+
[Test]
22+
public void Has_Thrown_And_Cancelled_Pipeline()
23+
{
24+
var exception = Assert.ThrowsAsync<ModuleFailedException>(async () => await RunModule<NonIgnoredFailureModule>());
25+
26+
var serviceProvider = exception!.Module.Context.Get<IServiceProvider>()!;
27+
var engineCancellationToken = serviceProvider.GetRequiredService<EngineCancellationToken>();
28+
29+
Assert.That(engineCancellationToken.IsCancellationRequested, Is.True);
30+
}
31+
}

0 commit comments

Comments
 (0)