Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
17d04c9
Started on two unit test methods for PingSession
DanielSpindler83 May 19, 2022
4016c16
Refactor, cleanup comments. Add assert for TestPingStatisticsDivideBy…
DanielSpindler83 May 19, 2022
73e5182
merge in dividebyzero fix
DanielSpindler83 May 19, 2022
87f9151
add asserts to main stats test and resolve some decimal rounding issues
DanielSpindler83 May 19, 2022
2481171
add in the divide by zero fix for AverageRoundTrip
DanielSpindler83 May 19, 2022
959bc0e
merge main into unit-testing - resolve conflicts manually.
DanielSpindler83 May 24, 2022
92145f6
Apply code style changes already done elsewhere.
DanielSpindler83 May 24, 2022
9443cf7
redesign PingSession to be stateful. No longer requires a list of Pin…
DanielSpindler83 May 24, 2022
d210dd1
Add command line arg ExportFile to PingRequestOptions - so we can use…
DanielSpindler83 May 24, 2022
3c7b265
remove old export code from program.cs
DanielSpindler83 May 24, 2022
e387fb9
redesign PingRequestAgent to use new stateful PingSession.
DanielSpindler83 May 24, 2022
9de8574
redesign unit tests to cater for new PingSession stateful design.
DanielSpindler83 May 24, 2022
3d5c170
change TotalRoundtrip to a double so we can round AverageRoundTrip to…
DanielSpindler83 May 24, 2022
0d226ea
remove unused pingRequests List
DanielSpindler83 May 24, 2022
4927bef
Add export file components to PingRequestAgent.
DanielSpindler83 May 24, 2022
4a60b6b
closes TurnerSoftware/Pingalot#6
DanielSpindler83 May 24, 2022
f2aed7c
create a TestPingResult method. https://github.com/TurnerSoftware/Pin…
DanielSpindler83 May 29, 2022
9cf489c
remove for loops creating test pingrequests.
DanielSpindler83 May 29, 2022
8a5c277
missed one test ping for ConfirmPingSessionStatistics. Added in.
DanielSpindler83 May 29, 2022
4adf00f
remove uneccesary link
DanielSpindler83 May 29, 2022
75876f4
experiment with single write file stream
DanielSpindler83 May 29, 2022
50106c2
add new -e flag for default export, refactor main perform pings loop,…
DanielSpindler83 Jul 21, 2022
ed1984c
setup and test FileExporter event
DanielSpindler83 Jul 24, 2022
8a925ab
add new -e flag for default export and refactor pingarguments
DanielSpindler83 Jul 24, 2022
fc6db54
file exporter working with event. No using and no dispose.
DanielSpindler83 Jul 27, 2022
a5082fa
tested and working export
DanielSpindler83 Aug 1, 2022
69b6ea5
merge in export event branch
DanielSpindler83 Aug 1, 2022
3e6d31d
remove unused export methods
DanielSpindler83 Aug 1, 2022
ab892e7
bug fix missing parameter
DanielSpindler83 Aug 1, 2022
51ba350
bug fix
DanielSpindler83 Aug 1, 2022
a092cfd
removed commented out code
DanielSpindler83 Aug 1, 2022
5ea55e7
added filexporter to traditional ping and tested
DanielSpindler83 Aug 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Pingalot.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{405D55
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F7E1DAB2-474F-47CE-83F0-82062D2CE69E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pingalot.Core.Tests", "tests\Pingalot.Core.Tests\Pingalot.Core.Tests.csproj", "{7F0E181A-6B70-4A96-96D1-70F19FB5FC9D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pingalot.Core.Tests", "tests\Pingalot.Core.Tests\Pingalot.Core.Tests.csproj", "{7F0E181A-6B70-4A96-96D1-70F19FB5FC9D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
2 changes: 1 addition & 1 deletion src/Pingalot.Core/PingRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ public class PingRequest
public int TimeToLive { get; init; }
public int BufferLength { get; init; }
public bool HasMatchingBuffer { get; init; }
public DateTime RequestTime { get; init; }
public DateTime? RequestTime { get; init; }
}
}
21 changes: 13 additions & 8 deletions src/Pingalot.Core/PingRequestAgent.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CsvHelper;

namespace Pingalot
{
Expand All @@ -18,15 +21,15 @@ public async Task<PingSession> StartAsync(PingRequestOptions options, Cancellati
{
Ttl = options.TimeTolive
};
var pingRequests = new List<PingRequest>();

var buffer = CreateBuffer(options.BufferSize);

var startTime = DateTime.Now;
var pingSession = new PingSession(startTime);

var timer = new Stopwatch();
timer.Start();

while (!cancellationToken.IsCancellationRequested && (options.NumberOfPings == -1 || pingRequests.Count < options.NumberOfPings))
while (!cancellationToken.IsCancellationRequested && (options.NumberOfPings == -1 || pingSession.PacketsSent < options.NumberOfPings))
{
var requestTime = DateTime.Now;
var pingReply = await pingSender.SendPingAsync(options.Address, (int)options.PingTimeout.TotalMilliseconds, buffer, pingOptions);
Expand All @@ -41,15 +44,15 @@ public async Task<PingSession> StartAsync(PingRequestOptions options, Cancellati
RequestTime = requestTime
};

pingRequests.Add(pingRequest);

var partialSession = new PingSession(startTime, timer.Elapsed, pingRequests);
PingCompleted?.Invoke(this, new PingCompletedEventArgs
{
CompletedPing = pingRequest,
Session = partialSession
Session = pingSession
});

pingSession.AddSinglePingResult(timer.Elapsed, pingRequest);

try
{
await Task.Delay(options.DelayBetweenPings, cancellationToken);
Expand All @@ -60,7 +63,8 @@ public async Task<PingSession> StartAsync(PingRequestOptions options, Cancellati
timer.Stop();
var endTime = DateTime.Now;

return new PingSession(startTime, endTime, timer.Elapsed, pingRequests);
pingSession.CalculateFinalPingStats(endTime, timer.Elapsed);
return pingSession;
}

private static byte[] CreateBuffer(int size)
Expand Down Expand Up @@ -95,5 +99,6 @@ private static bool CheckBuffer(byte[] expected, byte[] actual)

return true;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public PingRequestExportModel(PingRequest pingRequest)
TimeToLive = pingRequest.TimeToLive;
BufferLength = pingRequest.BufferLength;
HasMatchingBuffer = pingRequest.HasMatchingBuffer;
RequestTime = pingRequest.RequestTime.ToString("O");
RequestTime = pingRequest.RequestTime.ToString();
}
}
}
50 changes: 50 additions & 0 deletions src/Pingalot.Core/PingRequestFileExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CsvHelper;

namespace Pingalot
{
public class PingRequestFileExporter : IDisposable
{
private FileStream stream;
private StreamWriter writer;
private CsvWriter csv;

public PingRequestFileExporter(string ExportFileFullPath)
{

stream = File.Open(ExportFileFullPath, FileMode.Append,FileAccess.Write, FileShare.Read);
writer = new StreamWriter(stream, Encoding.UTF8, 1024, true);
csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteHeader<PingRequestExportModel>();
csv.NextRecord();

}

public void ExportSingleResultToFile(object sender, PingCompletedEventArgs pingCompletedEventArgs)
{
// write a single pingrequest record to export file
var singleExportablePingResult = new PingRequestExportModel(pingCompletedEventArgs.CompletedPing);
csv.WriteRecord(singleExportablePingResult);
csv.Flush();
csv.NextRecord();
}

public void Dispose()
{
csv.Dispose();
writer.Dispose();
stream.Dispose();
}

~PingRequestFileExporter()
{
this.Dispose();
}
}
}
24 changes: 24 additions & 0 deletions src/Pingalot.Core/PingRequestOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

Expand All @@ -14,5 +16,27 @@ public class PingRequestOptions
public int TimeTolive { get; init; }
public TimeSpan DelayBetweenPings { get; init; }
public int NumberOfPings { get; init; }

public string? ExportFileFullPath { get; set; }

public static string? SetExportFile(string ExportFileFullPath, bool UseExportFileDefault)
{
// if provided an export path then lets use it - it takes preference even if -e is set
// error check file path here...
if (ExportFileFullPath != null)
{
return ExportFileFullPath;
}

// no export path was provided, is -e set?
if (UseExportFileDefault)
{
var fileNameDateTime = DateTime.Now.ToString("yyyy-MM-dd__HH-mm-ss");
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\results_" + fileNameDateTime + ".csv";
}

// --export not specified and -e not set, this will return null - we dont want to export at all
return ExportFileFullPath;
}
}
}
89 changes: 46 additions & 43 deletions src/Pingalot.Core/PingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,68 @@ namespace Pingalot
public class PingSession
{
public DateTime StartTime { get; }
public DateTime? EndTime { get; }
public TimeSpan Elapsed { get; }
public IReadOnlyList<PingRequest> Requests { get; }
public DateTime? EndTime { get; private set; }
public TimeSpan Elapsed { get; private set; }

public PingSession(DateTime startTime, TimeSpan elapsed, IReadOnlyList<PingRequest> results)
{
StartTime = startTime;
Elapsed = elapsed;
Requests = results;
public PingRequest PingResult { get; private set; }

CalculateStatistics();
}
public int PacketsSent { get; private set; }
public int PacketsReceived { get; private set; }
public int PacketsLost { get; private set; }
public double PacketsLostPercentage { get; private set; }
public long MinimumRoundtrip { get; private set; }
public long MaximumRoundtrip { get; private set; }
public double TotalRoundtrip { get; private set; }
public double AverageRoundtrip { get; private set; }

public PingSession(DateTime startTime, DateTime endTime, TimeSpan elapsed, IReadOnlyList<PingRequest> results)
public PingSession(DateTime startTime)
{
StartTime = startTime;
EndTime = endTime;
Elapsed = elapsed;
Requests = results;

CalculateStatistics();
PacketsSent = 0;
PacketsReceived = 0;
PacketsLost = 0;
PacketsLostPercentage = 0D;
MinimumRoundtrip = 0L;
MaximumRoundtrip = 0L;
TotalRoundtrip = 0L;
AverageRoundtrip = 0D;
}

private void CalculateStatistics()
public void AddSinglePingResult(TimeSpan elapsed, PingRequest pingResult)
{
PacketsSent = Requests.Count;
var totalRoundtrip = 0d;
Elapsed = elapsed;
PingResult = pingResult;

for (var i = 0; i < Requests.Count; i++)
PacketsSent++;

if (PingResult.Status == IPStatus.Success)
{
var result = Requests[i];
PacketsReceived++;

if (result.Status == IPStatus.Success)
if (PacketsReceived == 1)
{
PacketsReceived++;
if (PacketsReceived == 1)
{
MinimumRoundtrip = result.RoundtripTime;
MaximumRoundtrip = result.RoundtripTime;
}
else
{
MinimumRoundtrip = Math.Min(MinimumRoundtrip, result.RoundtripTime);
MaximumRoundtrip = Math.Max(MaximumRoundtrip, result.RoundtripTime);
}

totalRoundtrip += result.RoundtripTime;
MinimumRoundtrip = PingResult.RoundtripTime;
MaximumRoundtrip = PingResult.RoundtripTime;
}
else
{
MinimumRoundtrip = Math.Min(MinimumRoundtrip, PingResult.RoundtripTime);
MaximumRoundtrip = Math.Max(MaximumRoundtrip, PingResult.RoundtripTime);
}

TotalRoundtrip += PingResult.RoundtripTime;
}

if (PacketsSent > 0)
{
PacketsLost = PacketsSent - PacketsReceived;
PacketsLostPercentage = (double)PacketsLost / PacketsSent * 100;
PacketsLostPercentage = Math.Round(PacketsLostPercentage, 2);


if (PacketsReceived > 0)
{
AverageRoundtrip = totalRoundtrip / PacketsReceived;
AverageRoundtrip = TotalRoundtrip / PacketsReceived;
AverageRoundtrip = Math.Round(AverageRoundtrip, 2);
}
else
Expand All @@ -74,12 +78,11 @@ private void CalculateStatistics()
}
}

public int PacketsSent { get; private set; }
public int PacketsReceived { get; private set; }
public int PacketsLost { get; private set; }
public double PacketsLostPercentage { get; private set; }
public long MinimumRoundtrip { get; private set; }
public long MaximumRoundtrip { get; private set; }
public double AverageRoundtrip { get; private set; }
public void CalculateFinalPingStats(DateTime endTime, TimeSpan elapsed)
{
EndTime = endTime;
Elapsed = elapsed;
}

}
}
4 changes: 4 additions & 0 deletions src/Pingalot.Core/Pingalot.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
<RootNamespace>Pingalot</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CsvHelper" Version="28.0.1" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/Pingalot/Layouts/ModernPing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public static async Task<PingSession> StartAsync(PingRequestOptions options)
var pingRequestAgent = new PingRequestAgent();
var cancellationTokenSource = new CancellationTokenSource();

if (options.ExportFileFullPath != null)
{
var pingRequestFileExporter = new PingRequestFileExporter(options.ExportFileFullPath);
pingRequestAgent.PingCompleted += pingRequestFileExporter.ExportSingleResultToFile;

}

Comment on lines 15 to +22
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see why you had trouble trying to dispose of it. A quick-and-dirty workaround would be to declare PingRequestFileExporter? pingRequestFileExporter = null; before the if-statement and then wrap the rest of the code in a try/finally. Then in the finally, we go pingRequestFileExporter?.Dispose();.

For a more complete solution, it might be a more ground-up redesign of how the classes interact with each other so we don't even need the file exporter within the specific visual implementations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean by the last part is that some of the difficulty is really just how the classes call each other. Ideal world would have something like each "layout" just register events rather than handle the life cycle itself. The ping session is kinda redundant now as we don't do anything with it. Remove the replication from the cancel key press etc logic in each layout.

If I were to name what I'm thinking, something like "pipelines". Each thing (a layout, exporting, etc) is just an item on the pipeline. I guess kinda like how the middleware works for ASP.NET Core but more around events than just calling a next(); action.

Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
Expand Down
8 changes: 8 additions & 0 deletions src/Pingalot/Layouts/TraditionalPing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public static async Task<PingSession> StartAsync(PingRequestOptions options)
};

var pingRequestAgent = new PingRequestAgent();

if (options.ExportFileFullPath != null)
{
var pingRequestFileExporter = new PingRequestFileExporter(options.ExportFileFullPath);
pingRequestAgent.PingCompleted += pingRequestFileExporter.ExportSingleResultToFile;

}

pingRequestAgent.PingCompleted += (sender, e) =>
{
var result = e.CompletedPing;
Expand Down
9 changes: 7 additions & 2 deletions src/Pingalot/PingArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Reflection;

namespace Pingalot
{
Expand Down Expand Up @@ -48,8 +50,11 @@ internal class PingArguments
[Option("layout", HelpText = "The display layout.\n1. Traditional\n2. Modern", Default = 2)]
public int Layout { get; set; }

[Option("export", HelpText = "The file to export the results to. Data is saved as a CSV.")]
public string ExportLocation { get; set; }
[Option("export", HelpText = "The full file path to export the results to. Data is saved as a CSV. Existing file is appended to. Overrides -e.", Default = null)]
public string? ExportFileFullPath { get; set; }

[Option('e', HelpText = "Export to CSV alongside this executable with default filename that includes current date & time.", Default = false)]
public bool UseExportFileDefault { get; set; }

public TimeSpan PingTimeout => new TimeSpan(0, 0, 0, 0, PingTimeoutInMilliseconds);
public TimeSpan BreakBetweenPings => new TimeSpan(0, 0, 0, 0, BreakBetweenPingsInMilliseconds);
Expand Down
2 changes: 1 addition & 1 deletion src/Pingalot/Pingalot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="CsvHelper" Version="19.0.0" />
<PackageReference Include="CsvHelper" Version="28.0.1" />
<PackageReference Include="Spectre.Console" Version="0.35.0" />
</ItemGroup>

Expand Down
Loading