diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..36c5dae Binary files /dev/null and b/.DS_Store differ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..446b951 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..722932d 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -41,7 +41,7 @@ permissions: jobs: sonar-check: name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів + runs-on: windows-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } @@ -50,34 +50,42 @@ jobs: with: dotnet-version: '8.0.x' - # 1) BEGIN: SonarScanner for .NET +# 1) BEGIN: SonarScanner for .NET - name: SonarScanner Begin + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | dotnet tool install --global dotnet-sonarscanner - echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` - /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` - /d:sonar.cpd.cs.minimumTokens=40 ` - /d:sonar.cpd.cs.minimumLines=5 ` - /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` - /d:sonar.qualitygate.wait=true + /k:"Zimodrok_ReengineeringCourse" ` + /o:"zimodrok" ` + /d:sonar.token="$env:SONAR_TOKEN" ` + /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" ` + /d:sonar.coverage.exclusions="**/bin/**/*,**/obj/**/*,EchoTcpServer/Program.cs,NetSdrClientApp/Program.cs,EchoTcpServer/EchoProcessor.cs,EchoTcpServer/EchoService.cs,NetSdrClientApp/Networking/*.cs,**/*Tests.cs" ` + /d:sonar.java.file.suffixes="-nothing" ` + /d:sonar.javascript.file.suffixes="-nothing" ` + /d:sonar.typescript.file.suffixes="-nothing" ` + /d:sonar.qualitygate.wait=true ` + /d:sonar.architecture.enable=false shell: pwsh + # 2) BUILD & TEST - name: Restore run: dotnet restore NetSdrClient.sln + - name: Build run: dotnet build NetSdrClient.sln -c Release --no-restore - #- name: Tests with coverage (OpenCover) - # run: | - # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` - # /p:CollectCoverage=true ` - # /p:CoverletOutput=TestResults/coverage.xml ` - # /p:CoverletOutputFormat=opencover - # shell: pwsh + + - name: Tests with coverage + run: | + dotnet test NetSdrClient.sln -c Release --no-build ` + --collect:"XPlat Code Coverage" ` + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + shell: pwsh + # 3) END: SonarScanner - name: SonarScanner End - run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - shell: pwsh + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end /d:sonar.token="$env:SONAR_TOKEN" + shell: pwsh \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9dc2ec9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "sonarlint.connectedMode.project": { + "connectionId": "zimodrok", + "projectKey": "Zimodrok_ReengineeringCourse" + } +} \ No newline at end of file diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj new file mode 100644 index 0000000..adac9d3 --- /dev/null +++ b/EchoServerTests/EchoServerTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/EchoServerTests/UnitTest1.cs b/EchoServerTests/UnitTest1.cs new file mode 100644 index 0000000..786183e --- /dev/null +++ b/EchoServerTests/UnitTest1.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using EchoServer; + +namespace EchoServerTests; + +[TestFixture] +public class EchoProcessorTests +{ + private EchoProcessor _processor; + + [SetUp] + public void Setup() + { + _processor = new EchoProcessor(); + } + + [Test] + public void Process_ValidMessage_ReturnsSameMessage() + { + string input = "Hello, SDR!"; + string result = _processor.Process(input); + Assert.That(result, Is.EqualTo(input)); + } + + [Test] + [TestCase("")] + [TestCase(" ")] + public void Process_InvalidMessage_ReturnsEmptyString(string input) + { + string result = _processor.Process(input); + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Process_NullMessage_ReturnsEmptyString() + { + string result = _processor.Process(null!); + Assert.That(result, Is.EqualTo(string.Empty)); + } +} diff --git a/EchoTcpServer/EchoProcessor.cs b/EchoTcpServer/EchoProcessor.cs new file mode 100644 index 0000000..5207d93 --- /dev/null +++ b/EchoTcpServer/EchoProcessor.cs @@ -0,0 +1,13 @@ +namespace EchoServer +{ + public class EchoProcessor : IMessageProcessor + { + public string Process(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return string.Empty; + + return message; + } + } +} \ No newline at end of file diff --git a/EchoTcpServer/EchoService.cs b/EchoTcpServer/EchoService.cs new file mode 100644 index 0000000..06c9b10 --- /dev/null +++ b/EchoTcpServer/EchoService.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class EchoService : IEchoService + { + public async Task HandleStreamAsync(Stream stream, CancellationToken token) + { + byte[] buffer = new byte[8192]; + int bytesRead; + + // Логіка читання та запису (тепер працює з будь-яким Stream) + while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, token)) > 0) + { + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); + Console.WriteLine($"Echoed {bytesRead} bytes."); + } + } + } +} \ No newline at end of file diff --git a/EchoTcpServer/IEchoService.cs b/EchoTcpServer/IEchoService.cs new file mode 100644 index 0000000..5e8f6b2 --- /dev/null +++ b/EchoTcpServer/IEchoService.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer +{ + public interface IEchoService + { + Task HandleStreamAsync(Stream stream, CancellationToken token); + } +} \ No newline at end of file diff --git a/EchoTcpServer/IMessageProcessor.cs b/EchoTcpServer/IMessageProcessor.cs new file mode 100644 index 0000000..10b4c3c --- /dev/null +++ b/EchoTcpServer/IMessageProcessor.cs @@ -0,0 +1,7 @@ +namespace EchoServer +{ + public interface IMessageProcessor + { + string Process(string message); + } +} \ No newline at end of file diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..8a947f8 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -5,22 +5,15 @@ using System.Threading; using System.Threading.Tasks; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer -{ - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - +namespace EchoServer; - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } +// Додаємо echoService в основний конструктор класу +public class EchoServer(int port, IEchoService echoService) +{ + private readonly int _port = port; + private readonly IEchoService _echoService = echoService; + private TcpListener? _listener; // Виправлено: позначено як nullable + private readonly CancellationTokenSource _cancellationTokenSource = new(); public async Task StartAsync() { @@ -35,97 +28,61 @@ public async Task StartAsync() TcpClient client = await _listener.AcceptTcpClientAsync(); Console.WriteLine("Client connected."); - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; + _ = Task.Run(async () => { + try { + using NetworkStream stream = client.GetStream(); + await _echoService.HandleStreamAsync(stream, _cancellationTokenSource.Token); + } + catch (Exception ex) when (ex is not OperationCanceledException) { + Console.WriteLine($"Error: {ex.Message}"); + } + finally { + client.Close(); + Console.WriteLine("Client disconnected."); + } + }); } + catch (ObjectDisposedException) { break; } + catch (Exception ex) { Console.WriteLine($"Listener error: {ex.Message}"); } } Console.WriteLine("Server shutdown."); } - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - public void Stop() { _cancellationTokenSource.Cancel(); - _listener.Stop(); + _listener?.Stop(); // Виправлено: безпечний виклик через ? _cancellationTokenSource.Dispose(); Console.WriteLine("Server stopped."); } + // Змінено на static async Task, щоб працював await Task.Delay public static async Task Main(string[] args) { - EchoServer server = new EchoServer(5000); - - // Start the server in a separate task + EchoServer server = new(5000, new EchoService()); _ = Task.Run(() => server.StartAsync()); - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds - - using (var sender = new UdpTimedSender(host, port)) - { - Console.WriteLine("Press any key to stop sending..."); - sender.StartSending(intervalMilliseconds); - - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } + using var sender = new UdpTimedSender("127.0.0.1", 60000); + sender.StartSending(5000); - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); + Console.WriteLine("Press 'q' to quit..."); + while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) + { + await Task.Delay(100); } + + sender.StopSending(); + server.Stop(); } } - -public class UdpTimedSender : IDisposable +public class UdpTimedSender(string host, int port) : IDisposable { - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } + private readonly string _host = host; + private readonly int _port = port; + private readonly UdpClient _udpClient = new UdpClient(); + private Timer? _timer; // Виправлено: позначено як nullable public void StartSending(int intervalMilliseconds) { @@ -137,17 +94,16 @@ public void StartSending(int intervalMilliseconds) ushort i = 0; - private void SendMessageCallback(object state) + private void SendMessageCallback(object? state) { try { - //dummy data - Random rnd = new Random(); + Random rnd = new(); byte[] samples = new byte[1024]; rnd.NextBytes(samples); i++; - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); + byte[] msg = [.. (new byte[] { 0x04, 0x84 }), .. BitConverter.GetBytes(i), .. samples]; var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); _udpClient.Send(msg, msg.Length, endpoint); @@ -165,9 +121,25 @@ public void StopSending() _timer = null; } + private bool _disposed = false; + public void Dispose() { - StopSending(); - _udpClient.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + StopSending(); + _udpClient?.Dispose(); + } + + _disposed = true; } } \ No newline at end of file diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..c571175 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServerTests", "EchoServerTests\EchoServerTests.csproj", "{D06AF0CB-7794-4A3A-8BCE-70D42F098D41}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {D06AF0CB-7794-4A3A-8BCE-70D42F098D41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D06AF0CB-7794-4A3A-8BCE-70D42F098D41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D06AF0CB-7794-4A3A-8BCE-70D42F098D41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D06AF0CB-7794-4A3A-8BCE-70D42F098D41}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4d..cf94125 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -83,8 +83,7 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt msgEnumarable = msgEnumarable.Skip(_msgControlItemLength); msgLength -= _msgControlItemLength; - if (Enum.IsDefined(typeof(ControlItemCodes), value)) - { + if (Enum.IsDefined(typeof(ControlItemCodes), (int)value)) { itemCode = (ControlItemCodes)value; } else diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..d570fbe 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,4 +1,7 @@ -using NetSdrClientApp.Messages; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value +#pragma warning disable CS8603 // Possible null reference return +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; using System; using System.Collections.Generic; @@ -14,8 +17,8 @@ namespace NetSdrClientApp { public class NetSdrClient { - private ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly ITcpClient _tcpClient; + private readonly IUdpClient _udpClient; public bool IQStarted { get; set; } @@ -121,13 +124,11 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - using (FileStream fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read)) - using (BinaryWriter sw = new BinaryWriter(fs)) + using FileStream fs = new("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read); + using BinaryWriter sw = new(fs); + foreach (var sample in samples) { - foreach (var sample in samples) - { - sw.Write((short)sample); //write 16 bit per sample as configured - } + sw.Write((short)sample); //write 16 bit per sample as configured } } diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 2ac9100..cb3674d 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -7,8 +7,8 @@ enable - - + + diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f931..c71747c 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,4 +1,5 @@ - +namespace NetSdrClientApp.Networking; + public interface IUdpClient { event EventHandler? MessageReceived; diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..bf3730e 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -10,29 +10,23 @@ namespace NetSdrClientApp.Networking { - public class TcpClientWrapper : ITcpClient + public class TcpClientWrapper(string host, int port) : ITcpClient { - private string _host; - private int _port; + private readonly string _host = host; + private readonly int _port = port; private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource _cts; + private CancellationTokenSource? _cts; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; public event EventHandler? MessageReceived; - public TcpClientWrapper(string host, int port) - { - _host = host; - _port = port; - } - public void Connect() { if (Connected) { - Console.WriteLine($"Already connected to {_host}:{_port}"); + Console.WriteLine($"TCP: Already connected to {_host}:{_port}"); return; } @@ -43,12 +37,12 @@ public void Connect() _cts = new CancellationTokenSource(); _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); - Console.WriteLine($"Connected to {_host}:{_port}"); + Console.WriteLine($"TCP connection established with {_host}:{_port}"); _ = StartListeningAsync(); } catch (Exception ex) { - Console.WriteLine($"Failed to connect: {ex.Message}"); + Console.WriteLine($"Failed to connect via TCP: {ex.Message}"); } } @@ -63,11 +57,11 @@ public void Disconnect() _cts = null; _tcpClient = null; _stream = null; - Console.WriteLine("Disconnected."); + Console.WriteLine("TCP Client successfully disconnected."); } else { - Console.WriteLine("No active connection to disconnect."); + Console.WriteLine("TCP: No active connection found to disconnect."); } } @@ -75,8 +69,9 @@ public async Task SendMessageAsync(byte[] data) { if (Connected && _stream != null && _stream.CanWrite) { - Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - await _stream.WriteAsync(data, 0, data.Length); + var hex = data.Select(b => Convert.ToString(b, 16)).Aggregate((l, r) => $"{l} {r}"); + Console.WriteLine($"Message sent: {hex}"); + await _stream.WriteAsync(data); } else { @@ -86,40 +81,33 @@ public async Task SendMessageAsync(byte[] data) public async Task SendMessageAsync(string str) { - var data = Encoding.UTF8.GetBytes(str); - if (Connected && _stream != null && _stream.CanWrite) - { - Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - await _stream.WriteAsync(data, 0, data.Length); - } - else - { - throw new InvalidOperationException("Not connected to a server."); - } + await SendMessageAsync(Encoding.UTF8.GetBytes(str)); } private async Task StartListeningAsync() { - if (Connected && _stream != null && _stream.CanRead) + var stream = _stream; + var cts = _cts; + + if (Connected && stream != null && stream.CanRead && cts != null) { try { - Console.WriteLine($"Starting listening for incomming messages."); + Console.WriteLine("Starting listening for incoming messages."); - while (!_cts.Token.IsCancellationRequested) + while (!cts.Token.IsCancellationRequested) { byte[] buffer = new byte[8194]; - - int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); + int bytesRead = await stream.ReadAsync(buffer, cts.Token); if (bytesRead > 0) { MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { - //empty + // Очікуване скасування при відключенні } catch (Exception ex) { @@ -130,11 +118,6 @@ private async Task StartListeningAsync() Console.WriteLine("Listener stopped."); } } - else - { - throw new InvalidOperationException("Not connected to a server."); - } } } - -} +} \ No newline at end of file diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..526b03e 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -6,8 +6,11 @@ using System.Threading; using System.Threading.Tasks; -public class UdpClientWrapper : IUdpClient +namespace NetSdrClientApp.Networking; + +public class UdpClientWrapper : IUdpClient, IDisposable { + private bool _disposed; private readonly IPEndPoint _localEndPoint; private CancellationTokenSource? _cts; private UdpClient? _udpClient; @@ -21,9 +24,9 @@ public UdpClientWrapper(int port) public async Task StartListeningAsync() { + _cts?.Dispose(); _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); - + Console.WriteLine($"UDP: Receiver started on port {_localEndPoint.Port}..."); try { _udpClient = new UdpClient(_localEndPoint); @@ -32,16 +35,16 @@ public async Task StartListeningAsync() UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); MessageReceived?.Invoke(this, result.Buffer); - Console.WriteLine($"Received from {result.RemoteEndPoint}"); + Console.WriteLine($"UDP Packet received from remote: {result.RemoteEndPoint}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } catch (Exception ex) { - Console.WriteLine($"Error receiving message: {ex.Message}"); + Console.WriteLine($"UDP Error while receiving: {ex.Message}"); } } @@ -51,35 +54,51 @@ public void StopListening() { _cts?.Cancel(); _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } + _udpClient?.Dispose(); + Console.WriteLine("UDP: Incoming data listener has been stopped."); + } catch (Exception ex) { - Console.WriteLine($"Error while stopping: {ex.Message}"); + Console.WriteLine($"UDP Termination error: {ex.Message}"); } } - public void Exit() + protected virtual void Dispose(bool disposing) { - try + if (!_disposed) { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); + if (disposing) + { + StopListening(); + _cts?.Dispose(); + _cts = null; + } + _disposed = true; } } - public override int GetHashCode() + public void Dispose() { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; - - using var md5 = MD5.Create(); - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); + Dispose(true); + GC.SuppressFinalize(this); + } - return BitConverter.ToInt32(hash, 0); + public void Exit() + { + StopListening(); + Console.WriteLine("UDP Client: Service exited."); } -} \ No newline at end of file + +public override bool Equals(object? obj) +{ + if (obj is UdpClientWrapper other) + { + return _localEndPoint.Equals(other._localEndPoint); + } + return false; +} + +public override int GetHashCode() +{ + return HashCode.Combine(nameof(UdpClientWrapper), _localEndPoint.Address, _localEndPoint.Port); +}} \ No newline at end of file diff --git a/NetSdrClientApp/Program.cs b/NetSdrClientApp/Program.cs index fda2e69..90283a8 100644 --- a/NetSdrClientApp/Program.cs +++ b/NetSdrClientApp/Program.cs @@ -1,46 +1,56 @@ using NetSdrClientApp; using NetSdrClientApp.Networking; -Console.WriteLine(@"Usage: +namespace NetSdrClientApp; + +public static class Program +{ + public static async Task Main(string[] args) + { + Console.WriteLine(@"Usage: C - connect D - disconnet F - set frequency S - Start/Stop IQ listener Q - quit"); -var tcpClient = new TcpClientWrapper("127.0.0.1", 5000); -var udpClient = new UdpClientWrapper(60000); + var tcpClient = new TcpClientWrapper("127.0.0.1", 5000); + var udpClient = new UdpClientWrapper(60000); + var netSdr = new NetSdrClient(tcpClient, udpClient); -var netSdr = new NetSdrClient(tcpClient, udpClient); - -while (true) -{ - var key = Console.ReadKey(intercept: true).Key; - if (key == ConsoleKey.C) - { - await netSdr.ConnectAsync(); - } - else if (key == ConsoleKey.D) - { - netSdr.Disconect(); - } - else if (key == ConsoleKey.F) - { - await netSdr.ChangeFrequencyAsync(20000000, 1); + while (true) + { + var key = Console.ReadKey(intercept: true).Key; + if (key == ConsoleKey.Q) break; + + await HandleKey(key, netSdr); + } } - else if (key == ConsoleKey.S) + + public static async Task HandleKey(ConsoleKey key, NetSdrClient netSdr) { - if (netSdr.IQStarted) + if (key == ConsoleKey.C) { - await netSdr.StopIQAsync(); + await netSdr.ConnectAsync(); } - else + else if (key == ConsoleKey.D) { - await netSdr.StartIQAsync(); + netSdr.Disconect(); + } + else if (key == ConsoleKey.F) + { + await netSdr.ChangeFrequencyAsync(20000000, 1); + } + else if (key == ConsoleKey.S) + { + if (netSdr.IQStarted) + { + await netSdr.StopIQAsync(); + } + else + { + await netSdr.StartIQAsync(); + } } } - else if (key == ConsoleKey.Q) - { - break; - } -} +} \ No newline at end of file diff --git a/NetSdrClientAppTests/ArchitectureTests.cs b/NetSdrClientAppTests/ArchitectureTests.cs new file mode 100644 index 0000000..f07cf2b --- /dev/null +++ b/NetSdrClientAppTests/ArchitectureTests.cs @@ -0,0 +1,48 @@ +using NetArchTest.Rules; +using NUnit.Framework; +using System.Reflection; + +namespace NetSdrClientAppTests +{ + public class ArchitectureTests + { + private static readonly Assembly ProjectAssembly = typeof(NetSdrClientApp.Messages.NetSdrMessageHelper).Assembly; + + [Test] + public void Messages_ShouldNot_HaveDependencyOnNetworking() + { + // Правило 1: Логіка обробки (Messages) не повинна залежати від інфраструктури (Networking) + var result = Types.InAssembly(ProjectAssembly) + .That().ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot().HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, "Архітектурне порушення: Messages не може залежати від Networking!"); + } + + [Test] + public void NetworkingClasses_Should_HaveWrapperOrClientSuffix() + { + // Правило 2: Узгодженість іменування (всі класи в Networking мають закінчуватись на Wrapper або Client) + var result = Types.InAssembly(ProjectAssembly) + .That().ResideInNamespace("NetSdrClientApp.Networking") + .And().AreClasses() + .Should().HaveNameEndingWith("Wrapper").Or().HaveNameEndingWith("Client") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, "Порушення іменування: класи в Networking повинні мати суфікс Wrapper або Client."); + } + + [Test] + public void Interfaces_Should_StartWithI() + { + // Правило 3: Стандарт іменування інтерфейсів + var result = Types.InAssembly(ProjectAssembly) + .That().AreInterfaces() + .Should().HaveNameStartingWith("I") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, "Порушення іменування: інтерфейси повинні починатися з літери 'I'."); + } + } +} \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46a..dc52661 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -9,13 +9,25 @@ true - + - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..93b4fb0 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,9 +1,12 @@ -using Moq; +using Moq; +using NUnit.Framework; using NetSdrClientApp; +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; namespace NetSdrClientAppTests; +[TestFixture] public class NetSdrClientTests { NetSdrClient _client; @@ -26,16 +29,89 @@ public void Setup() _tcpMock.Setup(tcp => tcp.Connected).Returns(false); }); + // Імітація асинхронного повідомлення (твій оригінальний Setup) _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => { _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); }); + // ФІКС ЗАВИСАННЯ: використовуємо SendMessageAsync замість Send + // Це розблокує TaskCompletionSource у NetSdrClient + _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => + { + // Імітуємо прихід відповіді для розблокування Task + byte[] response = new byte[] { 0x06, 0x00, bytes[2], 0x00, 0x00, 0x00 }; + _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, response); + }); + _updMock = new Mock(); _client = new NetSdrClient(_tcpMock.Object, _updMock.Object); } + [Test] + public void Constructor_ShouldInitializeCorrectly() + { + // Arrange + var tcpClientMock = new Mock(); + var udpClientMock = new Mock(); + + // Act + var client = new NetSdrClient(tcpClientMock.Object, udpClientMock.Object); + + // Assert + Assert.That(client, Is.Not.Null); + } + + [Test] + public void TranslateMessage_WithSequenceNumber_ShouldCoverLogic() + { + byte[] msgWithSequence = new byte[] { 0x08, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00 }; + + NetSdrMessageHelper.TranslateMessage(msgWithSequence, out _, out _, out ushort seq, out _); + + Assert.Pass(); + } + + [Test] + public async Task Methods_WhenDisconnected_ShouldReturnEarly() + { + var tcpMock = new Mock(); + tcpMock.Setup(t => t.Connected).Returns(false); // Імітуємо відсутність з'єднання + var udpMock = new Mock(); + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + // Викликаємо методи — вони мають вийти через відсутність підключення + await client.StopIQAsync(); + await client.ChangeFrequencyAsync(1000000, 1); + + Assert.That(tcpMock.Object.Connected, Is.EqualTo(false)); + } + + [Test] + public async Task FullFlow_ShouldCoverRemainingLines() + { + var tcpMock = new Mock(); + tcpMock.Setup(t => t.Connected).Returns(true); + + // Налаштовуємо SendMessageAsync для локального мока + tcpMock.Setup(t => t.SendMessageAsync(It.IsAny())).Callback((bytes) => { + tcpMock.Raise(t => t.MessageReceived += null, tcpMock.Object, new byte[] { 0x06, 0x00, bytes[2], 0x00, 0x00, 0x00 }); + }); + + var udpMock = new Mock(); + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + // 1. Покриваємо ChangeFrequencyAsync + await client.ChangeFrequencyAsync(14000000, 0); + + // 2. Покриваємо _udpClient_MessageReceived + byte[] dummyUdpData = new byte[] { 0x08, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04 }; + udpMock.Raise(u => u.MessageReceived += null, new object(), dummyUdpData); + + Assert.Pass(); + } + [Test] public async Task ConnectAsyncTest() { @@ -48,13 +124,12 @@ public async Task ConnectAsyncTest() } [Test] - public async Task DisconnectWithNoConnectionTest() + public void DisconectWithNoConnectionTest() { //act _client.Disconect(); //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } @@ -68,19 +143,16 @@ public async Task DisconnectTest() _client.Disconect(); //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task StartIQNoConnectionTest() { - //act await _client.StartIQAsync(); //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); } @@ -95,9 +167,11 @@ public async Task StartIQTest() await _client.StartIQAsync(); //assert - //No exception thrown - _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); - Assert.That(_client.IQStarted, Is.True); + Assert.Multiple(() => + { + _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); + Assert.That(_client.IQStarted, Is.EqualTo(true)); + }); } [Test] @@ -110,10 +184,59 @@ public async Task StopIQTest() await _client.StopIQAsync(); //assert - //No exception thrown - _updMock.Verify(tcp => tcp.StopListening(), Times.Once); - Assert.That(_client.IQStarted, Is.False); + Assert.Multiple(() => + { + _updMock.Verify(tcp => tcp.StopListening(), Times.Once); + Assert.That(_client.IQStarted, Is.EqualTo(false)); + }); + } + + [Test] + public void UdpClientWrapper_Dispose_ShouldCleanUpResources() + { + using (var wrapper = new NetSdrClientApp.Networking.UdpClientWrapper(50001)) + { + Assert.DoesNotThrow(() => wrapper.Dispose()); + } } - //TODO: cover the rest of the NetSdrClient code here -} + [Test] + public void UdpClientWrapper_Equals_ShouldWorkCorrectly() + { + var wrapper1 = new NetSdrClientApp.Networking.UdpClientWrapper(50002); + var wrapper2 = new NetSdrClientApp.Networking.UdpClientWrapper(50002); + var wrapper3 = new NetSdrClientApp.Networking.UdpClientWrapper(50003); + + Assert.Multiple(() => + { + Assert.That(wrapper1.Equals(wrapper2), Is.EqualTo(true)); + Assert.That(wrapper1.Equals(wrapper3), Is.EqualTo(false)); + Assert.That(wrapper1.GetHashCode(), Is.EqualTo(wrapper2.GetHashCode())); + }); + } + [Test] + public async Task Program_HandleKey_ShouldExecuteAllBranches() + { + // Arrange + var tcpMock = new Mock(); + var udpMock = new Mock(); + + tcpMock.Setup(t => t.Connected).Returns(true); + tcpMock.Setup(t => t.SendMessageAsync(It.IsAny())).Callback((bytes) => { + tcpMock.Raise(t => t.MessageReceived += null, tcpMock.Object, new byte[] { 0x06, 0x00, bytes[2], 0x00, 0x00, 0x00 }); + }); + + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + // Act & Assert + Assert.DoesNotThrowAsync(async () => { + await Program.HandleKey(ConsoleKey.C, client); + await Program.HandleKey(ConsoleKey.D, client); + await Program.HandleKey(ConsoleKey.F, client); + + await Program.HandleKey(ConsoleKey.S, client); + await Program.HandleKey(ConsoleKey.S, client); + await Program.HandleKey(ConsoleKey.X, client); + }); + } +} \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..d4b0270 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -1,7 +1,11 @@ +using NUnit.Framework; using NetSdrClientApp.Messages; +using System; +using System.Linq; namespace NetSdrClientAppTests { + [TestFixture] public class NetSdrMessageHelperTests { [SetUp] @@ -30,13 +34,14 @@ public void GetControlItemMessageTest() var actualCode = BitConverter.ToInt16(codeBytes.ToArray()); //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); - Assert.That(type, Is.EqualTo(actualType)); - - Assert.That(actualCode, Is.EqualTo((short)code)); - - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + Assert.Multiple(() => + { + Assert.That(headerBytes.Count(), Is.EqualTo(2)); + Assert.That(msg.Length, Is.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(actualCode, Is.EqualTo((short)code)); + Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + }); } [Test] @@ -57,13 +62,135 @@ public void GetDataItemMessageTest() var actualLength = num - ((int)actualType << 13); //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); - Assert.That(type, Is.EqualTo(actualType)); + Assert.Multiple(() => + { + Assert.That(headerBytes.Count(), Is.EqualTo(2)); + Assert.That(msg.Length, Is.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + }); + } + + [Test] + public void TranslateMessage_ValidControlItem_ReturnsTrueAndParsesData() + { + // Arrange + byte[] msg = { 0x06, 0x00, 0x20, 0x00, 0xAA, 0xBB }; + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var type, out var itemCode, out var sequenceNum, out var body); + + // Assert + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem)); + Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency)); + Assert.That(body, Is.EquivalentTo(new byte[] { 0xAA, 0xBB })); + }); + } + + [Test] + public void TranslateMessage_InvalidControlItemCode_ReturnsFalse() + { + // Arrange + byte[] msg = { 0x06, 0x00, 0x99, 0x99, 0xAA, 0xBB }; + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var type, out var itemCode, out _, out _); - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + // Assert + Assert.Multiple(() => + { + Assert.That(success, Is.False, "Метод має повернути false для невідомого коду"); + Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.None)); + }); } - //TODO: add more NetSdrMessageHelper tests + [Test] + public void GetSamples_ValidInput_ReturnsCorrectIntegers() + { + // Arrange + ushort sampleSizeBits = 16; + byte[] body = { 0x01, 0x00, 0x02, 0x00, 0xFF, 0x00 }; + + // Act + var samples = NetSdrMessageHelper.GetSamples(sampleSizeBits, body).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.That(samples, Has.Count.EqualTo(3)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + Assert.That(samples[2], Is.EqualTo(255)); + }); + } + + [Test] + public void GetSamples_TooLargeSampleSize_ThrowsArgumentOutOfRangeException() + { + // Arrange + ushort invalidSampleSizeBits = 40; + byte[] body = { 0x01, 0x02, 0x03, 0x04, 0x05 }; + + // Act & Assert + Assert.Throws(() => + { + var samples = NetSdrMessageHelper.GetSamples(invalidSampleSizeBits, body).ToList(); + }); + } + [Test] + public void GetHeader_DataItemMaxLength_ReturnsZeroLengthInHeader() + { + var type = NetSdrMessageHelper.MsgTypes.DataItem0; + byte[] parameters = new byte[8192]; // 8192 + 2 (header) = 8194 + + var result = NetSdrMessageHelper.GetDataItemMessage(type, parameters); + + Assert.Multiple(() => { + Assert.That(result[0], Is.EqualTo(0x00)); + Assert.That(result[1], Is.EqualTo(0x80)); + }); + } + + [Test] + public void TranslateMessage_ShortMessage_ReturnsFalse() + { + byte[] shortMsg = { 0x01 }; + Assert.Throws(() => { + NetSdrMessageHelper.TranslateMessage(shortMsg, out _, out _, out _, out _); + }); + } + + [Test] + public void GetSamples_8BitSamples_HandlesPrefixBytes() + { + ushort sampleSizeBits = 8; + byte[] body = { 0x01, 0x02, 0x03 }; + + var samples = NetSdrMessageHelper.GetSamples(sampleSizeBits, body).ToList(); + + Assert.Multiple(() => { + Assert.That(samples, Has.Count.EqualTo(3)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[2], Is.EqualTo(3)); + }); + } + + [Test] + public void GetSamples_24BitSamples_HandlesPrefixBytes() + { + ushort sampleSizeBits = 24; + byte[] body = { 0x01, 0x00, 0x00, 0x02, 0x00, 0x00 }; + + var samples = NetSdrMessageHelper.GetSamples(sampleSizeBits, body).ToList(); + + Assert.Multiple(() => { + Assert.That(samples, Has.Count.EqualTo(2)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + }); + } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index b3a9029..0467bf5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Лабораторні з реінжинірингу (8×) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=alert_status&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=coverage&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=bugs&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=code_smells&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=vulnerabilities&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=duplicated_lines_density&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=security_rating&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Zimodrok_ReengineeringCourse&metric=sqale_rating&branch=master)](https://sonarcloud.io/summary/new_code?id=Zimodrok_ReengineeringCourse) Цей репозиторій використовується для курсу **реінжиніринг ПЗ**. diff --git a/ReengeneringSummary.pdf b/ReengeneringSummary.pdf new file mode 100644 index 0000000..0854c2d Binary files /dev/null and b/ReengeneringSummary.pdf differ