Skip to content

Commit 57d90c0

Browse files
[feature] Add skipped inventory aggregates for issue 267 (#279)
1 parent 94c7593 commit 57d90c0

File tree

8 files changed

+176
-25
lines changed

8 files changed

+176
-25
lines changed

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ This is a C#/.NET 8 solution with a client-server architecture:
2020

2121
### Overview
2222
This repository uses a branch-based development workflow. Please follow the conventions below when working in this project, especially when creating branches, writing PR titles, and generating commits or documentation.
23-
Agents must not create branches until explicitly instructed to do so by a human.
23+
Agents must not create branches until they have received clear instructions to do so from a human or an explicitly
24+
invoked skill.
2425

2526
### Branch Naming
2627
Use the following prefixes:

src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public class InventoryProcessData : ReactiveObject
1313
{
1414
private readonly object _monitorDataLock = new object();
1515
private readonly ConcurrentQueue<SkippedEntry> _skippedEntries = new();
16+
private readonly ConcurrentDictionary<SkipReason, int> _skippedCountsByReason = new();
17+
private int _skippedCount;
1618

1719
public InventoryProcessData()
1820
{
@@ -105,6 +107,8 @@ public List<Inventory>? Inventories
105107

106108
public IReadOnlyCollection<SkippedEntry> SkippedEntries => _skippedEntries.ToArray();
107109

110+
public int SkippedCount => _skippedCount;
111+
108112
[Reactive]
109113
public DateTimeOffset InventoryStart { get; set; }
110114

@@ -141,8 +145,16 @@ public void Reset()
141145
public void RecordSkippedEntry(SkippedEntry entry)
142146
{
143147
_skippedEntries.Enqueue(entry);
148+
_skippedCountsByReason.AddOrUpdate(entry.Reason, 1, (_, currentCount) => currentCount + 1);
149+
Interlocked.Increment(ref _skippedCount);
144150
}
145-
151+
152+
// should be used during issue 268 implementation
153+
public int GetSkippedCountByReason(SkipReason reason)
154+
{
155+
return _skippedCountsByReason.GetValueOrDefault(reason, 0);
156+
}
157+
146158
public void SetError(Exception exception)
147159
{
148160
LastException = exception;
@@ -166,6 +178,8 @@ private void ClearSkippedEntries()
166178
while (_skippedEntries.TryDequeue(out _))
167179
{
168180
}
181+
182+
_skippedCountsByReason.Clear();
183+
Interlocked.Exchange(ref _skippedCount, 0);
169184
}
170-
}
171-
185+
}

src/ByteSync.Client/Models/Inventories/InventoryPart.cs

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,40 @@ public InventoryPart()
1010
{
1111
FileDescriptions = new List<FileDescription>();
1212
DirectoryDescriptions = new List<DirectoryDescription>();
13+
SkippedCountsByReason = new Dictionary<SkipReason, int>();
1314
}
14-
15+
1516
public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inventoryPartType) : this()
1617
{
1718
Inventory = inventory;
1819
RootPath = rootPath;
1920
InventoryPartType = inventoryPartType;
2021
}
21-
22+
2223
public Inventory Inventory { get; set; }
23-
24+
2425
public string RootPath { get; set; }
25-
26+
2627
public FileSystemTypes InventoryPartType { get; set; }
27-
28+
2829
public string Code { get; set; }
29-
30+
3031
public List<FileDescription> FileDescriptions { get; set; }
31-
32+
3233
public List<DirectoryDescription> DirectoryDescriptions { get; set; }
33-
34+
3435
public bool IsIncompleteDueToAccess { get; set; }
35-
36+
37+
public Dictionary<SkipReason, int> SkippedCountsByReason
38+
{
39+
get;
40+
41+
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
42+
set => field = value ?? new Dictionary<SkipReason, int>();
43+
}
44+
45+
public int SkippedCount => SkippedCountsByReason.Values.Sum();
46+
3647
public string RootName
3748
{
3849
get
@@ -42,63 +53,77 @@ public string RootName
4253
{
4354
case OSPlatforms.Windows:
4455
directorySeparatorChar = "\\";
56+
4557
break;
4658
case OSPlatforms.Linux:
4759
case OSPlatforms.MacOs:
4860
directorySeparatorChar = "/";
61+
4962
break;
5063
default:
5164
throw new ArgumentOutOfRangeException(nameof(directorySeparatorChar));
5265
}
53-
66+
5467
return RootPath.Substring(RootPath.LastIndexOf(directorySeparatorChar, StringComparison.Ordinal));
5568
}
5669
}
57-
70+
5871
protected bool Equals(InventoryPart other)
5972
{
6073
return Equals(Inventory, other.Inventory) && RootPath == other.RootPath && InventoryPartType == other.InventoryPartType;
6174
}
62-
75+
6376
public override bool Equals(object obj)
6477
{
6578
if (ReferenceEquals(null, obj)) return false;
6679
if (ReferenceEquals(this, obj)) return true;
6780
if (obj.GetType() != this.GetType()) return false;
68-
return Equals((InventoryPart) obj);
81+
82+
return Equals((InventoryPart)obj);
6983
}
70-
84+
7185
public override int GetHashCode()
7286
{
7387
unchecked
7488
{
7589
var hashCode = Inventory.GetHashCode();
7690
hashCode = (hashCode * 397) ^ RootPath.GetHashCode();
77-
hashCode = (hashCode * 397) ^ (int) InventoryPartType;
91+
hashCode = (hashCode * 397) ^ (int)InventoryPartType;
92+
7893
return hashCode;
7994
}
8095
}
81-
96+
8297
public override string ToString()
8398
{
8499
#if DEBUG
85100
return $"InventoryPart {RootName} {RootPath}";
86101
#endif
87-
102+
88103
#pragma warning disable 162
89104
return base.ToString();
90105
#pragma warning restore 162
91106
}
92-
107+
93108
public void AddFileSystemDescription(FileSystemDescription fileSystemDescription)
94109
{
95110
if (fileSystemDescription.FileSystemType == FileSystemTypes.File)
96111
{
97-
FileDescriptions.Add((FileDescription) fileSystemDescription);
112+
FileDescriptions.Add((FileDescription)fileSystemDescription);
98113
}
99114
else
100115
{
101-
DirectoryDescriptions.Add((DirectoryDescription) fileSystemDescription);
116+
DirectoryDescriptions.Add((DirectoryDescription)fileSystemDescription);
102117
}
103118
}
104-
}
119+
120+
public int GetSkippedCountByReason(SkipReason reason)
121+
{
122+
return SkippedCountsByReason.TryGetValue(reason, out var count) ? count : 0;
123+
}
124+
125+
public void RecordSkippedEntry(SkipReason reason)
126+
{
127+
SkippedCountsByReason[reason] = GetSkippedCountByReason(reason) + 1;
128+
}
129+
}

src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ private void RecordSkippedEntry(InventoryPart inventoryPart, FileSystemInfo file
691691
DetectedKind = detectedKind
692692
};
693693

694+
inventoryPart.RecordSkippedEntry(reason);
694695
InventoryProcessData.RecordSkippedEntry(entry);
695696
}
696697

tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using ByteSync.Business.Inventories;
4+
using ByteSync.Models.Inventories;
45
using FluentAssertions;
56
using NUnit.Framework;
67

@@ -25,4 +26,40 @@ public void SetError_ShouldUpdateLastException_AndRaiseEvent()
2526
data.LastException.Should().Be(exception);
2627
values.Should().Contain(true);
2728
}
29+
30+
[Test]
31+
public void RecordSkippedEntry_ShouldUpdateGlobalAndReasonCounters()
32+
{
33+
// Arrange
34+
var data = new InventoryProcessData();
35+
36+
// Act
37+
data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden });
38+
data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden });
39+
data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.NoiseEntry });
40+
41+
// Assert
42+
data.SkippedCount.Should().Be(3);
43+
data.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2);
44+
data.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1);
45+
data.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0);
46+
}
47+
48+
[Test]
49+
public void Reset_ShouldClearSkippedEntriesAndCounters()
50+
{
51+
// Arrange
52+
var data = new InventoryProcessData();
53+
data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden });
54+
data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.NoiseEntry });
55+
56+
// Act
57+
data.Reset();
58+
59+
// Assert
60+
data.SkippedEntries.Should().BeEmpty();
61+
data.SkippedCount.Should().Be(0);
62+
data.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(0);
63+
data.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(0);
64+
}
2865
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using ByteSync.Models.Inventories;
2+
using FluentAssertions;
3+
using NUnit.Framework;
4+
5+
namespace ByteSync.Client.UnitTests.Models.Inventories;
6+
7+
[TestFixture]
8+
public class InventoryPartSkippedCountsTests
9+
{
10+
[Test]
11+
public void RecordSkippedEntry_ShouldUpdateTotalAndReasonCounts()
12+
{
13+
// Arrange
14+
var part = new InventoryPart();
15+
16+
// Act
17+
part.RecordSkippedEntry(SkipReason.Hidden);
18+
part.RecordSkippedEntry(SkipReason.Hidden);
19+
part.RecordSkippedEntry(SkipReason.NoiseEntry);
20+
21+
// Assert
22+
part.SkippedCount.Should().Be(3);
23+
part.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2);
24+
part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1);
25+
part.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0);
26+
}
27+
28+
[Test]
29+
public void SkippedCountsByReason_WhenSetToNull_ShouldFallbackToEmptyDictionary()
30+
{
31+
// Arrange
32+
var part = new InventoryPart();
33+
part.SkippedCountsByReason = null!;
34+
35+
// Act
36+
var skippedCount = part.SkippedCount;
37+
var hiddenCount = part.GetSkippedCountByReason(SkipReason.Hidden);
38+
39+
// Assert
40+
skippedCount.Should().Be(0);
41+
hiddenCount.Should().Be(0);
42+
}
43+
}

tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,12 @@ public async Task Noise_Child_File_Is_Recorded()
267267
var invPath = Path.Combine(TestDirectory.FullName, "inv_noise_child.zip");
268268
await builder.BuildBaseInventoryAsync(invPath);
269269

270+
var part = builder.Inventory.InventoryParts.Single();
271+
270272
processData.SkippedEntries.Should()
271273
.ContainSingle(e => e.Name == "thumbs.db" && e.Reason == SkipReason.NoiseEntry);
274+
part.SkippedCount.Should().Be(1);
275+
part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1);
272276
}
273277

274278
[Test]
@@ -306,6 +310,8 @@ public async Task Noise_Child_Directory_Is_Recorded_And_Not_Traversed()
306310

307311
processData.SkippedEntries.Should()
308312
.ContainSingle(e => e.Name == "$RECYCLE.BIN" && e.Reason == SkipReason.NoiseEntry);
313+
part.SkippedCount.Should().Be(1);
314+
part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1);
309315
}
310316

311317
[Test]
@@ -387,6 +393,8 @@ public async Task Offline_Root_File_Is_Recorded()
387393
part.FileDescriptions.Should().BeEmpty();
388394
processData.SkippedEntries.Should()
389395
.ContainSingle(e => e.Name == "offline.txt" && e.Reason == SkipReason.Offline);
396+
part.SkippedCount.Should().Be(1);
397+
part.GetSkippedCountByReason(SkipReason.Offline).Should().Be(1);
390398
}
391399

392400
[Test]

tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ public void Load_ShouldMarkPartIncomplete_WhenInaccessibleDescriptionsExist()
4545
loadedInventory.InventoryParts[0].IsIncompleteDueToAccess.Should().BeTrue();
4646
}
4747

48+
[Test]
49+
public void Load_ShouldKeepSkippedCountsByReason()
50+
{
51+
// Arrange
52+
var inventory = BuildInventoryWithInaccessibleDirectory();
53+
var part = inventory.InventoryParts[0];
54+
part.RecordSkippedEntry(SkipReason.Hidden);
55+
part.RecordSkippedEntry(SkipReason.Hidden);
56+
part.RecordSkippedEntry(SkipReason.NoiseEntry);
57+
var zipPath = CreateInventoryZipFile(_tempDirectory, inventory);
58+
59+
// Act
60+
using var loader = new InventoryLoader(zipPath);
61+
var loadedPart = loader.Inventory.InventoryParts[0];
62+
63+
// Assert
64+
loadedPart.SkippedCount.Should().Be(3);
65+
loadedPart.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2);
66+
loadedPart.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1);
67+
loadedPart.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0);
68+
}
69+
4870
private static Inventory BuildInventoryWithInaccessibleDirectory()
4971
{
5072
var inventory = new Inventory

0 commit comments

Comments
 (0)