diff --git a/Pingalot.sln b/Pingalot.sln index 0ecf58a..e92b032 100644 --- a/Pingalot.sln +++ b/Pingalot.sln @@ -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 diff --git a/src/Pingalot.Core/PingRequest.cs b/src/Pingalot.Core/PingRequest.cs index 37cdfb4..f99ae30 100644 --- a/src/Pingalot.Core/PingRequest.cs +++ b/src/Pingalot.Core/PingRequest.cs @@ -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; } } } diff --git a/src/Pingalot.Core/PingRequestAgent.cs b/src/Pingalot.Core/PingRequestAgent.cs index e4c344c..453df0b 100644 --- a/src/Pingalot.Core/PingRequestAgent.cs +++ b/src/Pingalot.Core/PingRequestAgent.cs @@ -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 { @@ -18,15 +21,15 @@ public async Task StartAsync(PingRequestOptions options, Cancellati { Ttl = options.TimeTolive }; - var pingRequests = new List(); 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); @@ -41,15 +44,15 @@ public async Task 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); @@ -60,7 +63,8 @@ public async Task 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) @@ -95,5 +99,6 @@ private static bool CheckBuffer(byte[] expected, byte[] actual) return true; } + } } diff --git a/src/Pingalot/PingRequestExportModel.cs b/src/Pingalot.Core/PingRequestExportModel.cs similarity index 93% rename from src/Pingalot/PingRequestExportModel.cs rename to src/Pingalot.Core/PingRequestExportModel.cs index 8779f13..57c66b4 100644 --- a/src/Pingalot/PingRequestExportModel.cs +++ b/src/Pingalot.Core/PingRequestExportModel.cs @@ -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(); } } } diff --git a/src/Pingalot.Core/PingRequestFileExporter.cs b/src/Pingalot.Core/PingRequestFileExporter.cs new file mode 100644 index 0000000..9a6c372 --- /dev/null +++ b/src/Pingalot.Core/PingRequestFileExporter.cs @@ -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(); + 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(); + } + } +} diff --git a/src/Pingalot.Core/PingRequestOptions.cs b/src/Pingalot.Core/PingRequestOptions.cs index d786247..90bef26 100644 --- a/src/Pingalot.Core/PingRequestOptions.cs +++ b/src/Pingalot.Core/PingRequestOptions.cs @@ -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; @@ -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; + } } } diff --git a/src/Pingalot.Core/PingSession.cs b/src/Pingalot.Core/PingSession.cs index b9aed5c..f39a93c 100644 --- a/src/Pingalot.Core/PingSession.cs +++ b/src/Pingalot.Core/PingSession.cs @@ -7,64 +7,68 @@ namespace Pingalot public class PingSession { public DateTime StartTime { get; } - public DateTime? EndTime { get; } - public TimeSpan Elapsed { get; } - public IReadOnlyList Requests { get; } + public DateTime? EndTime { get; private set; } + public TimeSpan Elapsed { get; private set; } - public PingSession(DateTime startTime, TimeSpan elapsed, IReadOnlyList 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 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 @@ -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; + } + } } \ No newline at end of file diff --git a/src/Pingalot.Core/Pingalot.Core.csproj b/src/Pingalot.Core/Pingalot.Core.csproj index 4e4e56e..0f76320 100644 --- a/src/Pingalot.Core/Pingalot.Core.csproj +++ b/src/Pingalot.Core/Pingalot.Core.csproj @@ -5,4 +5,8 @@ Pingalot + + + + diff --git a/src/Pingalot/Layouts/ModernPing.cs b/src/Pingalot/Layouts/ModernPing.cs index 0242a30..43ebdbc 100644 --- a/src/Pingalot/Layouts/ModernPing.cs +++ b/src/Pingalot/Layouts/ModernPing.cs @@ -13,6 +13,13 @@ public static async Task 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; + + } + Console.CancelKeyPress += (sender, e) => { e.Cancel = true; diff --git a/src/Pingalot/Layouts/TraditionalPing.cs b/src/Pingalot/Layouts/TraditionalPing.cs index ed07fad..39afa22 100644 --- a/src/Pingalot/Layouts/TraditionalPing.cs +++ b/src/Pingalot/Layouts/TraditionalPing.cs @@ -18,6 +18,14 @@ public static async Task 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; diff --git a/src/Pingalot/PingArguments.cs b/src/Pingalot/PingArguments.cs index 8d32d98..9796603 100644 --- a/src/Pingalot/PingArguments.cs +++ b/src/Pingalot/PingArguments.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Reflection; namespace Pingalot { @@ -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); diff --git a/src/Pingalot/Pingalot.csproj b/src/Pingalot/Pingalot.csproj index 76e5b5b..fe9789a 100644 --- a/src/Pingalot/Pingalot.csproj +++ b/src/Pingalot/Pingalot.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Pingalot/Program.cs b/src/Pingalot/Program.cs index ebc7809..5062b93 100644 --- a/src/Pingalot/Program.cs +++ b/src/Pingalot/Program.cs @@ -1,10 +1,5 @@ using CommandLine; -using CsvHelper; using Pingalot.Layouts; -using System; -using System.Globalization; -using System.IO; -using System.Linq; using System.Threading.Tasks; namespace Pingalot @@ -13,6 +8,7 @@ class Program { static async Task Main(string[] args) { + await Parser.Default.ParseArguments(args) .WithParsedAsync(async pingArgs => { @@ -23,9 +19,12 @@ await Parser.Default.ParseArguments(args) DelayBetweenPings = pingArgs.BreakBetweenPings, PingTimeout = pingArgs.PingTimeout, TimeTolive = pingArgs.TimeToLive, - NumberOfPings = pingArgs.PingUntilStopped ? -1 : pingArgs.NumberOfPings + NumberOfPings = pingArgs.PingUntilStopped ? -1 : pingArgs.NumberOfPings, + ExportFileFullPath = PingRequestOptions.SetExportFile(pingArgs.ExportFileFullPath, pingArgs.UseExportFileDefault) }; + + //TODO: Error on bad layout PingSession session = null; if (pingArgs.Layout == Layout.Traditional) @@ -37,19 +36,6 @@ await Parser.Default.ParseArguments(args) session = await ModernPing.StartAsync(options); } - if (session != null) - { - if (!string.IsNullOrWhiteSpace(pingArgs.ExportLocation)) - { - Console.WriteLine("Exporting results to {0}...", pingArgs.ExportLocation); - using (var file = File.Open(pingArgs.ExportLocation, FileMode.Create)) - using (var writer = new StreamWriter(file)) - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - await csv.WriteRecordsAsync(session.Requests.Select(r => new PingRequestExportModel(r))); - } - } - } }); } } diff --git a/src/Pingalot/Properties/launchSettings.json b/src/Pingalot/Properties/launchSettings.json index 3e8c75a..a6e7cad 100644 --- a/src/Pingalot/Properties/launchSettings.json +++ b/src/Pingalot/Properties/launchSettings.json @@ -2,7 +2,27 @@ "profiles": { "PingStats": { "commandName": "Project", - "commandLineArgs": "-t --layout 2 --export test.csv 1.1.1.1" + "commandLineArgs": "-t --layout 2 -e 1.1.1.1" + }, + "--export": { + "commandName": "Project", + "commandLineArgs": "-t --layout 2 --export c:\\temp\\testexport.csv 1.1.1.1" + }, + "no export": { + "commandName": "Project", + "commandLineArgs": "-t --layout 2 1.1.1.1" + }, + "trad -e": { + "commandName": "Project", + "commandLineArgs": "-t --layout 1 -e 1.1.1.1" + }, + "trad --export": { + "commandName": "Project", + "commandLineArgs": "-t --layout 1 --export C:\\temp\\trad.csv 1.1.1.1" + }, + "trad no export": { + "commandName": "Project", + "commandLineArgs": "-t --layout 1 1.1.1.1" } } } \ No newline at end of file diff --git a/tests/Pingalot.Core.Tests/PingSessionTests.cs b/tests/Pingalot.Core.Tests/PingSessionTests.cs index c9b414c..f623980 100644 --- a/tests/Pingalot.Core.Tests/PingSessionTests.cs +++ b/tests/Pingalot.Core.Tests/PingSessionTests.cs @@ -17,26 +17,67 @@ public void TestPingStatisticsDivideByZero() var pingRequests = new List(); var startTime = new DateTime(2022, 5, 19, 14, 30, 0); - var duration = TimeSpan.FromSeconds(30); - var endTime = startTime.Add(duration); + var duration = new TimeSpan(0, 0, 0, 30); + var testPingSession = new PingSession(startTime); - var pingRequest = new PingRequest + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.TimedOut,0) ); + + // When we have a single failed ping request - our AverageRoundtrip should be 0 + Assert.AreEqual(testPingSession.AverageRoundtrip, 0, "If PacketsReceived is 0, when we attempt to calculate AverageRoundTrip we may trigger a divide by zero exception."); + } + + [TestMethod] + public void ConfirmPingSessionStatistics() + { + var startTime = new DateTime(2022, 5, 19, 14, 30, 0); + var duration = new TimeSpan(0, 0, 0, 30); + + var testPingSession = new PingSession(startTime); + + // Create and add 6 successful pings 10ms RT + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 10)); + + // Create and add 3 successful pings 5ms RT + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 5)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 5)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.Success, 5)); + + // Create and add 2 TimedOut pings + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.TimedOut, 0)); + testPingSession.AddSinglePingResult(duration, CreateTestPingResult(IPStatus.TimedOut, 0)); + + // assert that stats add up properly - calc them manually and not via debug + Assert.AreEqual(testPingSession.PacketsReceived, 9, "PacketsReceived should be 9."); + Assert.AreEqual(testPingSession.PacketsSent, 11, "PacketsSent should be 11."); + Assert.AreEqual(testPingSession.PacketsLost, 2, "PacketsLost should be 2."); + Assert.AreEqual(testPingSession.PacketsLostPercentage, 18.18, "PacketsLostPercentage should be 18.18%"); + Assert.AreEqual(testPingSession.MinimumRoundtrip, 5, "MinimumRoundtrip should be 5"); + Assert.AreEqual(testPingSession.MaximumRoundtrip, 10, "MinimumRoundtrip should be 10"); + Assert.AreEqual(testPingSession.AverageRoundtrip, 8.33, "AverageRoundtrip should equal 75 divided by 9 = 8.33 rounded to two decimal places."); + } + + + private static PingRequest CreateTestPingResult(IPStatus status, long roundtripTime) + { + + var newPingRequest = new PingRequest { Address = IPAddress.Loopback, - Status = IPStatus.TimedOut, - RoundtripTime = 0, - TimeToLive = 0, - BufferLength = 0, - HasMatchingBuffer = false, - RequestTime = DateTime.Now + Status = status, + RoundtripTime = roundtripTime, + TimeToLive = (status == IPStatus.Success) ? 60 : 0, + BufferLength = (status == IPStatus.Success) ? 32 : 0, + HasMatchingBuffer = (status == IPStatus.Success), + RequestTime = null }; - pingRequests.Add(pingRequest); - - var testPingSession = new PingSession(startTime, endTime, duration, pingRequests); - - Assert.AreEqual(testPingSession.AverageRoundtrip, 0, "If PacketsReceived is 0, when we attempt to calculate AverageRoundTrip we may trigger a divide by zero exception."); + return newPingRequest; } } } diff --git a/tests/Pingalot.Core.Tests/Pingalot.Core.Tests.csproj b/tests/Pingalot.Core.Tests/Pingalot.Core.Tests.csproj index e81a357..5939f9c 100644 --- a/tests/Pingalot.Core.Tests/Pingalot.Core.Tests.csproj +++ b/tests/Pingalot.Core.Tests/Pingalot.Core.Tests.csproj @@ -7,6 +7,7 @@ +