-
Notifications
You must be signed in to change notification settings - Fork 0
TEST3 MyChat #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
TEST3 MyChat #9
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // <copyright file="ArgumentsTests.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyChat.Test; | ||
|
|
||
| using MyChat; | ||
| using NUnit.Framework; | ||
|
|
||
| /// <summary> | ||
| /// Unit tests for <see cref="Arguments"/> parsing logic. | ||
| /// </summary> | ||
| public class ArgumentsTests | ||
| { | ||
| /// <summary> | ||
| /// Tests that parsing a single argument returns server mode. | ||
| /// </summary> | ||
| [Test] | ||
| public void Arguments_Parse_SingleArgument_ReturnsServerMode() | ||
| { | ||
| var args = new[] { "5000" }; | ||
| var parsed = Arguments.Parse(args); | ||
|
|
||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(parsed.IsServer, Is.True); | ||
| Assert.That(parsed.Port, Is.EqualTo(5000)); | ||
| Assert.That(parsed.IpAddress, Is.Null); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that parsing two arguments returns client mode. | ||
| /// </summary> | ||
| [Test] | ||
| public void Arguments_Parse_TwoArguments_ReturnsClientMode() | ||
| { | ||
| var args = new[] { "127.0.0.1", "5000" }; | ||
| var parsed = Arguments.Parse(args); | ||
|
|
||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(parsed.IsServer, Is.False); | ||
| Assert.That(parsed.Port, Is.EqualTo(5000)); | ||
| Assert.That(parsed.IpAddress, Is.EqualTo("127.0.0.1")); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // <copyright file="ChatRunnerTests.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyChat.Test; | ||
|
|
||
| using System.IO; | ||
| using System.Net; | ||
| using System.Net.Sockets; | ||
| using System.Threading.Tasks; | ||
| using MyChat; | ||
| using NUnit.Framework; | ||
|
|
||
| /// <summary> | ||
| /// Tests for <see cref="ChatRunner"/>. | ||
| /// </summary> | ||
| [TestFixture] | ||
| public class ChatRunnerTests | ||
| { | ||
| /// <summary> | ||
| /// Tests that a server and client can exchange a single message. | ||
| /// </summary> | ||
| /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> | ||
| [Test] | ||
| public async Task ChatRunner_RunAsync_ServerReceive() | ||
| { | ||
| const string ipAddress = "127.0.0.1"; | ||
| const int port = 5005; | ||
|
|
||
| string serverReceived = string.Empty; | ||
| string clientMessage = "Hello!"; | ||
|
|
||
| var serverTask = Task.Run(async () => | ||
| { | ||
| var listener = new TcpListener(IPAddress.Parse(ipAddress), port); | ||
| listener.Start(); | ||
|
|
||
| using var client = await listener.AcceptTcpClientAsync(); | ||
| using var stream = client.GetStream(); | ||
| using var reader = new StreamReader(stream); | ||
| using var writer = new StreamWriter(stream) { AutoFlush = true }; | ||
|
|
||
| serverReceived = await reader.ReadLineAsync() ?? string.Empty; | ||
| await writer.WriteLineAsync(serverReceived); | ||
|
|
||
| listener.Stop(); | ||
| }); | ||
|
|
||
| await Task.Delay(200); | ||
|
|
||
| var clientTask = Task.Run(async () => | ||
| { | ||
| using var client = new TcpClient(); | ||
| await client.ConnectAsync(IPAddress.Parse(ipAddress), port); | ||
| using var stream = client.GetStream(); | ||
| using var reader = new StreamReader(stream); | ||
| using var writer = new StreamWriter(stream) { AutoFlush = true }; | ||
|
|
||
| await writer.WriteLineAsync(clientMessage); | ||
| var received = await reader.ReadLineAsync()!; | ||
| return received; | ||
| }); | ||
|
|
||
| var clientReceived = await clientTask; | ||
| await serverTask; | ||
|
|
||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(serverReceived, Is.EqualTo(clientMessage)); | ||
| Assert.That(clientReceived, Is.EqualTo(clientMessage)); | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <LangVersion>latest</LangVersion> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsPackable>false</IsPackable> | ||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="coverlet.collector" Version="6.0.2"/> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/> | ||
| <PackageReference Include="NUnit" Version="4.2.2"/> | ||
| <PackageReference Include="NUnit.Analyzers" Version="4.4.0"/> | ||
| <PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Using Include="NUnit.Framework"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556"> | ||
| <PrivateAssets>all</PrivateAssets> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| </PackageReference> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <AdditionalFiles Include="stylecop.json" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\MyChat\MyChat.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", | ||
| "settings": { | ||
| "documentationRules": { | ||
| "companyName": "khusainovilas", | ||
| "copyrightText": "Copyright (c) {companyName}. All rights reserved." | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat", "MyChat\MyChat.csproj", "{DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat.Tests", "MyChat.Tests\MyChat.Tests.csproj", "{A801DB41-1484-46AA-9D09-0CFEB18C7271}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| EndGlobal |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // <copyright file="Arguments.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyChat; | ||
|
|
||
| using System; | ||
|
|
||
| /// <summary> | ||
| /// Represents parsed command-line arguments for the chat application. | ||
| /// </summary> | ||
| public class Arguments | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Думаю, что можно было сделать эту штуку record-ом или хотя бы сделать основной конструктор |
||
| { | ||
| private Arguments(int port, string? ipAddress) | ||
| { | ||
| this.Port = port; | ||
| this.IpAddress = ipAddress; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the network port number. | ||
| /// </summary> | ||
| public int Port { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the IP address for client mode. | ||
| /// </summary> | ||
| public string? IpAddress { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether returns the value, is it a server or an application. | ||
| /// </summary> | ||
| public bool IsServer => this.IpAddress is null; | ||
|
|
||
| /// <summary> | ||
| /// Parses command-line arguments. | ||
| /// </summary> | ||
| /// <param name="args">Command-line arguments.</param> | ||
| /// <returns>Parsed <see cref="Arguments"/> instance.</returns> | ||
| public static Arguments Parse(string[] args) | ||
| { | ||
| if (args.Length == 1) | ||
| { | ||
| return new Arguments(int.Parse(args[0]), null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Стоило бы заодно проверить, принадлежит ли номер порта разрешённому диапазону, чтобы выдать пользователю понятную диагностику. |
||
| } | ||
|
|
||
| if (args.Length == 2) | ||
| { | ||
| return new Arguments(int.Parse(args[1]), args[0]); | ||
| } | ||
|
|
||
| throw new ArgumentException("Invalid number of command-line arguments."); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // <copyright file="ChatRunner.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyChat; | ||
|
|
||
| using System; | ||
| using System.IO; | ||
| using System.Net; | ||
| using System.Net.Sockets; | ||
| using System.Threading.Tasks; | ||
|
|
||
| /// <summary> | ||
| /// Provides methods for running a chat application in server or client mode. | ||
| /// </summary> | ||
| public static class ChatRunner | ||
| { | ||
| private static readonly List<TcpClient> ConnectedClients = []; | ||
|
|
||
| /// <summary> | ||
| /// Starts the chat application based on the provided arguments. | ||
| /// </summary> | ||
| /// <param name="arguments">Arguments containing port and optional IP address.</param> | ||
| /// <returns>A <see cref="Task"/> representing the async operation.</returns> | ||
| public static async Task RunAsync(Arguments arguments) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Очеь желательно всё, что Async, делать отменяемым (принимать CancellationToken опциональным параметром, чтобы кому надо — могли операцию отменить). Тут и в методах ниже. |
||
| { | ||
| if (arguments.IsServer) | ||
| { | ||
| await RunServerAsync(arguments.Port); | ||
| } | ||
| else | ||
| { | ||
| await RunClientAsync(arguments.IpAddress!, arguments.Port); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs the chat application as a server listening. | ||
| /// </summary> | ||
| /// <param name="port">Port number to listen on.</param> | ||
| /// <returns>Representing the asynchronous server operation.</returns> | ||
| public static async Task RunServerAsync(int port) | ||
| { | ||
| var listener = new TcpListener(IPAddress.Any, port); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TcpListener IDisposable, поэтому лучше его объявлять с using. Иначе из-за какого-нибудь исключения listener.Stop может не исполниться и порт останется занят до конца работы программы. |
||
| listener.Start(); | ||
| Console.WriteLine($"Server listening on port {port}..."); | ||
|
|
||
| while (true) | ||
| { | ||
| var client = await listener.AcceptTcpClientAsync(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Оставлять потенциально бесконечное по времени действие неотменяемым очень страшно, тут бы CancellationToken передавать |
||
| lock (ConnectedClients) | ||
| { | ||
| ConnectedClients.Add(client); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. По условию клиент всего один, так что это зря :) |
||
| } | ||
|
|
||
| var endpoint = client.Client.RemoteEndPoint?.ToString() ?? "Unknown"; | ||
|
|
||
| Console.WriteLine($"Client connected: {endpoint}"); | ||
|
|
||
| _ = Task.Run(async () => | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. А так Вы теряете ссылку на запущенную задачу, следовательно не можете её ни прервать, ни дождаться её завершения |
||
| { | ||
| await RunChatAsync(client, endpoint); | ||
|
|
||
| Console.WriteLine($"Client disconnected: {endpoint}"); | ||
| lock (ConnectedClients) | ||
| { | ||
| ConnectedClients.Remove(client); | ||
| } | ||
|
|
||
| lock (ConnectedClients) | ||
| { | ||
| if (ConnectedClients.Count == 0) | ||
| { | ||
| Console.WriteLine("No clients connected. Server shutting down."); | ||
| Environment.Exit(0); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это вообще не стоит использовать — вдруг сервер захотят переиспользовать в большем приложении |
||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs the chat application as a client. | ||
| /// </summary> | ||
| /// <param name="ip">IP address of the server to connect to.</param> | ||
| /// <param name="port">Port number of the server.</param> | ||
| /// <returns>Representing the async client operation.</returns> | ||
| public static async Task RunClientAsync(string ip, int port) | ||
| { | ||
| using var client = new TcpClient(); | ||
| await client.ConnectAsync(ip, port); | ||
| Console.WriteLine($"Connected to server {ip}:{port}"); | ||
| await RunChatAsync(client, "Server"); | ||
| } | ||
|
|
||
| private static async Task RunChatAsync(TcpClient client, string name) | ||
| { | ||
| using var stream = client.GetStream(); | ||
| using var reader = new StreamReader(stream); | ||
| using var writer = new StreamWriter(stream) { AutoFlush = true }; | ||
|
|
||
| var consoleTask = Task.Run(async () => | ||
| { | ||
| while (true) | ||
| { | ||
| var line = Console.ReadLine(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это тоже надо было бы сделать отменяемым. Иначе клиент уже отключился, а мы сидим тут и ждём, пока пользователь что-то введёт, хотя уже давно должны были закрыть программу. У Вас эта проблема решается с помощью Environment.Exit, но это всё равно что выключатель света в доме сломался, мы дом снесли к чертям. |
||
| if (line == null || line.Equals("exit", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| await writer.WriteLineAsync(line); | ||
| } | ||
| }); | ||
|
|
||
| var networkTask = Task.Run(async () => | ||
| { | ||
| while (true) | ||
| { | ||
| var line = await reader.ReadLineAsync(); | ||
| if (line == null) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| Console.WriteLine($"> {line}"); | ||
| } | ||
| }); | ||
|
|
||
| await Task.WhenAny(consoleTask, networkTask); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. По-хорошему надо корректно остановить и дождаться завершения обеих задач |
||
| client.Close(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Удивительно, но клиент и сервер в тесте даже не участвуют, тестируются TcpListener и TcpClient из стандартной библиотеки