• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ImmediatePlatform / Immediate.Jobs / 30669186804

31 Jul 2026 10:12PM UTC coverage: 83.933% (+0.1%) from 83.82%
30669186804

Pull #86

github

web-flow
Merge 29013d26d into c8caa3f4e
Pull Request #86: Add job cancellation APIs and dashboard action

101 of 114 new or added lines in 9 files covered. (88.6%)

5 existing lines in 2 files now uncovered.

8207 of 9778 relevant lines covered (83.93%)

2.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.1
/src/Immediate.Jobs.Testing/CaptureOnlyJobScheduler.cs
1
namespace Immediate.Jobs.Testing;
2

3
/// <summary>
4
/// A storage-free implementation of <see cref="IJobScheduler{TPayload}"/> that records scheduled calls.
5
/// </summary>
6
/// <typeparam name="TPayload">The job payload type.</typeparam>
7
/// <param name="timeProvider">The optional clock used to determine immediate and delayed due times.</param>
8
public class CaptureOnlyJobScheduler<TPayload>(TimeProvider? timeProvider = null) : IJobScheduler<TPayload>
4✔
9
{
10
        private readonly List<ScheduledJobCapture<TPayload>> _captures = [];
4✔
11
        private readonly HashSet<string> _cancelledIds = new(StringComparer.Ordinal);
4✔
12
        private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
4✔
13

14
        /// <summary>All calls captured in call order.</summary>
15
        /// <value>The captured scheduler calls.</value>
16
        public IReadOnlyList<ScheduledJobCapture<TPayload>> Captures => _captures;
4✔
17

18
        /// <summary>The latest captured call, or <see langword="null"/> when none exists.</summary>
19
        /// <value>The latest captured call, or <see langword="null"/>.</value>
20
        public ScheduledJobCapture<TPayload>? Last => _captures.Count == 0 ? null : _captures[^1];
×
21

22
        /// <summary>The identifiers explicitly cancelled through this scheduler.</summary>
23
        /// <value>The cancelled invocation identifiers.</value>
24
        public IReadOnlySet<string> CancelledIds => _cancelledIds;
4✔
25

26
        /// <inheritdoc />
27
        public virtual ValueTask CancelAsync(JobHandle handle, CancellationToken cancellationToken = default)
28
        {
29
                ArgumentException.ThrowIfNullOrWhiteSpace(handle.Id, nameof(handle));
4✔
30
                cancellationToken.ThrowIfCancellationRequested();
4✔
31
                if (!_captures.Any(capture => string.Equals(capture.Id, handle.Id, StringComparison.Ordinal)))
4✔
NEW
32
                        throw new KeyNotFoundException($"Job '{handle.Id}' was not found.");
×
33
                if (!_cancelledIds.Add(handle.Id))
4✔
NEW
34
                        throw new ImmediateJobException("Only a non-terminal job can be cancelled.");
×
35
                return ValueTask.CompletedTask;
4✔
36
        }
37

38
        /// <inheritdoc />
39
        public virtual ValueTask<JobHandle> EnqueueAsync(TPayload payload, CancellationToken cancellationToken = default) =>
40
                CaptureAsync(payload, _timeProvider.GetUtcNow(), groupId: null, cancellationToken: cancellationToken);
×
41

42
        /// <inheritdoc />
43
        public virtual ValueTask<JobHandle> EnqueueAsync(
44
                TPayload payload,
45
                string? groupId,
46
                CancellationToken cancellationToken
47
        ) => CaptureAsync(payload, _timeProvider.GetUtcNow(), groupId, cancellationToken);
4✔
48

49
        /// <inheritdoc />
50
        public virtual ValueTask<JobHandle> ScheduleAsync(
51
                TPayload payload,
52
                TimeSpan delay,
53
                CancellationToken cancellationToken = default
54
        )
55
        {
56
                if (delay < TimeSpan.Zero)
×
57
                        throw new ArgumentOutOfRangeException(nameof(delay), "A job delay cannot be negative.");
×
58
                return CaptureAsync(payload, _timeProvider.GetUtcNow() + delay, groupId: null, cancellationToken);
×
59
        }
60

61
        /// <inheritdoc />
62
        public virtual ValueTask<JobHandle> ScheduleAsync(
63
                TPayload payload,
64
                TimeSpan delay,
65
                string? groupId,
66
                CancellationToken cancellationToken
67
        )
68
        {
69
                if (delay < TimeSpan.Zero)
4✔
70
                        throw new ArgumentOutOfRangeException(nameof(delay), "A job delay cannot be negative.");
×
71
                return CaptureAsync(payload, _timeProvider.GetUtcNow() + delay, groupId, cancellationToken);
4✔
72
        }
73

74
        /// <inheritdoc />
75
        public virtual ValueTask<JobHandle> ScheduleAtAsync(
76
                TPayload payload,
77
                DateTimeOffset runAt,
78
                CancellationToken cancellationToken = default
79
        ) => CaptureAsync(payload, runAt, groupId: null, cancellationToken: cancellationToken);
×
80

81
        /// <inheritdoc />
82
        public virtual ValueTask<JobHandle> ScheduleAtAsync(
83
                TPayload payload,
84
                DateTimeOffset runAt,
85
                string? groupId,
86
                CancellationToken cancellationToken
87
        ) => CaptureAsync(payload, runAt, groupId, cancellationToken);
4✔
88

89
        /// <summary>Clears every captured call and cancellation.</summary>
90
        public void Clear()
91
        {
NEW
92
                _captures.Clear();
×
NEW
93
                _cancelledIds.Clear();
×
NEW
94
        }
×
95

96
        /// <summary>Creates invocation identifiers. Override when a test requires predictable identifiers.</summary>
97
        /// <returns>A new invocation identifier.</returns>
98
        protected virtual string CreateId() => Guid.NewGuid().ToString("N");
4✔
99

100
        private ValueTask<JobHandle> CaptureAsync(
101
                TPayload payload,
102
                DateTimeOffset runAt,
103
                string? groupId,
104
                CancellationToken cancellationToken
105
        )
106
        {
107
                cancellationToken.ThrowIfCancellationRequested();
4✔
108
                groupId = NormalizeGroupId(groupId);
4✔
109
                var id = CreateId();
4✔
110
                _captures.Add(new(id, payload, runAt) { GroupId = groupId });
4✔
111
                return ValueTask.FromResult(new JobHandle(id));
4✔
112
        }
113

114
        private static string? NormalizeGroupId(string? groupId)
115
        {
116
                if (string.IsNullOrWhiteSpace(groupId))
4✔
117
                        return null;
×
118
                if (groupId.Length > 128)
4✔
119
                        throw new ArgumentException("A fair queue group id cannot exceed 128 characters.", nameof(groupId));
×
120
                return groupId;
4✔
121
        }
122
}
123

124
/// <summary>A captured typed scheduler call.</summary>
125
/// <typeparam name="TPayload">The captured payload type.</typeparam>
126
/// <param name="Id">The captured invocation identifier.</param>
127
/// <param name="Payload">The captured payload.</param>
128
/// <param name="RunAt">The captured absolute due time.</param>
129
public sealed record ScheduledJobCapture<TPayload>(string Id, TPayload Payload, DateTimeOffset RunAt)
4✔
130
{
131
        /// <summary>The normalized fair queue group id supplied to the scheduler.</summary>
132
        /// <value>The normalized group identifier, or <see langword="null"/>.</value>
133
        public string? GroupId { get; init; }
134
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc