Skip to content
Open

Kr2 #55

Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions C#/forSpbu/Md5Calculator.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using NUnit.Framework;
24 changes: 24 additions & 0 deletions C#/forSpbu/Md5Calculator.Tests/Md5Calculator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Md5Calculator\Md5Calculator.csproj" />
</ItemGroup>

</Project>
160 changes: 160 additions & 0 deletions C#/forSpbu/Md5Calculator.Tests/Md5CalculatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
namespace Md5Calculator.Tests;

public class Md5CalculatorTests
{
private static IEnumerable<Func<string, byte[]>> ComputeInstances()
{
yield return Md5Calculator.Compute;
yield return path => Md5Calculator.ComputeAsync(path).Result;
}

[Test]
public void AsyncEqualsSyncTest()
{
var dirNames = new []
{
"./AsyncEqualsSyncTest",
"./AsyncEqualsSyncTest/Dir1",
};
(string path, string data)[] files = new []
{
("./AsyncEqualsSyncTest/Dir1/file1.txt", "Asd"),
("./AsyncEqualsSyncTest/file1.txt", "Asd"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(Md5Calculator.Compute(dirNames[0]), Is.EqualTo(Md5Calculator.ComputeAsync(dirNames[0]).Result));

files.AsParallel()
.ForAll(file => File.Delete(file.path));
Directory.Delete(dirNames[1]);
Comment on lines +30 to +32

Choose a reason for hiding this comment

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

Имело смысл это в try/finally обернуть, или вынести в SetUp/TearDown, иначе при исключении в тестируемом коде тест оставит за собой временные файлы.

}

[Test, TestCaseSource(nameof(ComputeInstances))]
public void RecalculateEqualsTest(Func<string, byte[]> compute)
{
var dirNames = new []
{
"./RecalculateEqualsTest",
"./RecalculateEqualsTest/Dir1",
};
(string path, string data)[] files = new []
{
("./RecalculateEqualsTest/Dir1/file1.txt", "Asd"),
("./RecalculateEqualsTest/file1.txt", "Asd"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(compute(dirNames[0]), Is.EqualTo(compute(dirNames[0])));

Choose a reason for hiding this comment

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

Анализатор ругается, а не должен. Можно было бы переписать этот фрагмент кода, чтобы убрать предупреждение.


files.AsParallel()
.ForAll(file => File.Delete(file.path));
Directory.Delete(dirNames[1]);
}

[Test, TestCaseSource(nameof(ComputeInstances))]
public void FileNameMattersTest(Func<string, byte[]> compute)

Choose a reason for hiding this comment

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

По условию как раз имя файла в формуле для хеша не участвовало, но ладно, это скорее дефект условия

{
var dirNames = new []
{
"./FileNameMattersTest",
};
(string path, string data)[] files = new []
{
("./FileNameMattersTest/file2.txt", "Asd"),
("./FileNameMattersTest/file1.txt", "Asd"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(compute(files[0].path), Is.Not.EqualTo(compute(files[1].path)));

files.AsParallel()
.ForAll(file => File.Delete(file.path));
}

[Test, TestCaseSource(nameof(ComputeInstances))]
public void DirNameMattersTest(Func<string, byte[]> compute)
{
var dirNames = new []
{
"./DirNameMattersTest/Dir1",
"./DirNameMattersTest/Dirs5",
};
(string path, string data)[] files = new []
{
("./DirNameMattersTest/Dir1/file1.txt", "Asd"),
("./DirNameMattersTest/Dirs5/file1.txt", "Asd"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1])));

files.AsParallel()
.ForAll(file => File.Delete(file.path));
dirNames.AsParallel().ForAll(path => Directory.Delete(path));
}

[Test, TestCaseSource(nameof(ComputeInstances))]
public void DirFilesMatterTest(Func<string, byte[]> compute)
{
var dirNames = new []
{
"./DirFilesMatterTest/Dir1",
"./DirFilesMatterTest/Dir2",
};
(string path, string data)[] files = new []
{
("./DirFilesMatterTest/Dir1/file1.txt", "Asd"),
("./DirFilesMatterTest/Dir2/file1.txt", "Asde"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1])));

files.AsParallel()
.ForAll(file => File.Delete(file.path));
dirNames.AsParallel().ForAll(path => Directory.Delete(path));
}

[Test, TestCaseSource(nameof(ComputeInstances))]
public void SubDirsMatterTest(Func<string, byte[]> compute)
{
var dirNames = new []
{
"./SubDirsMatterTest/Dir1",
"./SubDirsMatterTest/Dir2",
"./SubDirsMatterTest/Dir1/Dir11",
"./SubDirsMatterTest/Dir2/Dir21",
};
(string path, string data)[] files = new []
{
("./SubDirsMatterTest/Dir1/Dir11/file1.txt", "Asd"),
("./SubDirsMatterTest/Dir2/Dir21/file1.txt", "Asde"),
};
dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path));
files.AsParallel()
.ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data)));

Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1])));

files.AsParallel()
.ForAll(file => File.Delete(file.path));
Directory.Delete(dirNames[3]);
Directory.Delete(dirNames[2]);
Directory.Delete(dirNames[1]);
Directory.Delete(dirNames[0]);



Comment on lines +156 to +158

Choose a reason for hiding this comment

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

Лишние пустые строки

}
}
22 changes: 22 additions & 0 deletions C#/forSpbu/Md5Calculator/CalculatorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Md5Calculator;

/// <summary>
/// Represents exception thrown in Md5 calculator
/// </summary>
public class CalculatorException : ArgumentException
{
/// <summary>
/// Creates exception with message
/// </summary>
/// <param name="message">Exception message</param>
public CalculatorException(string message) : base(message)
{
}

/// <summary>
/// Creates empty exception
/// </summary>
public CalculatorException()
{
}
}
83 changes: 83 additions & 0 deletions C#/forSpbu/Md5Calculator/Md5Calculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace Md5Calculator;

using System.Security.Cryptography;

/// <summary>
/// Represents md5 hash calculator
/// </summary>
public static class Md5Calculator
{
/// <summary>
/// Asynchronously computes md5 hash of given path
/// </summary>
/// <param name="path">Path to compute</param>
/// <returns>Hash as byte array</returns>
/// <exception cref="CalculatorException">If given does not exist</exception>
public static async Task<byte[]> ComputeAsync(string path)
{
if (!Directory.Exists(path) && !File.Exists(path))
{
throw new CalculatorException("Path doesnt exist");
}

return !Directory.Exists(path) ? await ComputeFileAsync(path) : await ComputeDirAsync(path);
}

/// <summary>
/// Synchronously computes md5 hash of given path
/// </summary>
/// <param name="path">Path to compute</param>
/// <returns>Hash as byte array</returns>
/// <exception cref="CalculatorException">If given does not exist</exception>
public static byte[] Compute(string path)
{
if (!Directory.Exists(path) && !File.Exists(path))
{
throw new CalculatorException("Path doesnt exist");
}

return !Directory.Exists(path) ? ComputeFile(path) : ComputeDir(path);
}


private static async Task<byte[]> ComputeDirAsync(string path)
{
var fileTasks = Directory.GetFiles(path).Select(ComputeFileAsync);

var dirTasks = Directory.GetDirectories(path).Select(ComputeDirAsync);
Comment on lines +45 to +47

Choose a reason for hiding this comment

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

Directory.GetFiles не фиксирует порядок возвращаемых файлов, так что их сначала надо было отсортировать, иначе значение хеша будет зависеть от непредсказуемых внешних факторов (типа удалили и добавили тот же файл).


var tasks = fileTasks.Concat(dirTasks).ToArray();

var overallBytes = (await Task.WhenAll(tasks))
.SelectMany(byteArray => byteArray)
.Concat(ComputeString(new DirectoryInfo(path).Name))

Choose a reason for hiding this comment

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

По условию от имени директории хеш не считается, она добавляется как есть. Впрочем, это особо не важно, просто лишняя работа.

.ToArray();
return MD5.HashData(overallBytes);
}

private static byte[] ComputeDir(string path)
{
var fileBytes = Directory.GetFiles(path).SelectMany(ComputeFile);

var dirBytes = Directory.GetDirectories(path).SelectMany(ComputeDir);

var overallBytes = fileBytes.Concat(dirBytes).Concat(ComputeString(new DirectoryInfo(path).Name)).ToArray();

return MD5.HashData(overallBytes);
}

private static async Task<byte[]> ComputeFileAsync(string path)
{
var byteArray = (await MD5.HashDataAsync(new FileInfo(path).OpenRead())).Concat(ComputeString(new FileInfo(path).Name)).ToArray();

Choose a reason for hiding this comment

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

Чтобы всё было вообще идеально в плане асинхронности, надо было ещё CancellationToken принимать.

return MD5.HashData(byteArray);
}

private static byte[] ComputeFile(string path)

Choose a reason for hiding this comment

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

Однопоточная версия тоже могла бы (да и должна была, по идее) использовать ComputeFileAsync. Асинхронность и многопоточность — разные вещи.

{
var byteArray = MD5.HashData(new FileInfo(path).OpenRead()).Concat(ComputeString(new FileInfo(path).Name)).ToArray();
return MD5.HashData(byteArray);
}

private static byte[] ComputeString(string @string) =>
MD5.HashData(System.Text.Encoding.ASCII.GetBytes(@string));
}
11 changes: 11 additions & 0 deletions C#/forSpbu/Md5Calculator/Md5Calculator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Md5_calculator</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
29 changes: 29 additions & 0 deletions C#/forSpbu/Md5Calculator/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Md5Calculator;
using System.Diagnostics;

if (args.Length != 1 || string.IsNullOrEmpty(args[0]))
{
Console.WriteLine("Should be 1 argument with path");
return;
}

try
{
var startTime = Stopwatch.GetTimestamp();
var hash = Md5Calculator.Md5Calculator.Compute(args[0]);
var endTime = Stopwatch.GetTimestamp();
Console.WriteLine($"Single thread: {Convert.ToBase64String(hash)}, {Stopwatch.GetElapsedTime(startTime, endTime)}");

startTime = Stopwatch.GetTimestamp();
hash = await Md5Calculator.Md5Calculator.ComputeAsync(args[0]);
endTime = Stopwatch.GetTimestamp();
Console.WriteLine($"Multi thread: {Convert.ToBase64String(hash)}, {Stopwatch.GetElapsedTime(startTime, endTime)}");
}
catch (CalculatorException e)
{
Console.WriteLine(e.Message);
}
catch (IOException e)
{
Console.WriteLine(e.Message);
}
27 changes: 23 additions & 4 deletions C#/forSpbu/forSpbu.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03.03", "03.03", "{882A9B9C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "10.03", "10.03", "{EA6FC7D9-BDFB-49CD-AC00-FC5DDC5274B0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2023", "2023", "{318796AF-A927-4A13-BAEE-FD13551DE91A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "30.11", "30.11", "{47EB60F8-9C89-4D13-8243-B77B01A4BA53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Md5Calculator", "Md5Calculator\Md5Calculator.csproj", "{AA2AD188-B903-46DF-AE43-4769DBF788E9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Md5Calculator.Tests", "Md5Calculator.Tests\Md5Calculator.Tests.csproj", "{6A71CD77-320C-458F-97B3-BA8F4CB530C6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,12 +29,23 @@ Global
{E007586F-9760-4744-BB25-EDEFD6BA860C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E007586F-9760-4744-BB25-EDEFD6BA860C}.Release|Any CPU.Build.0 = Release|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.Build.0 = Release|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.Build.0 = Release|Any CPU
{AA2AD188-B903-46DF-AE43-4769DBF788E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA2AD188-B903-46DF-AE43-4769DBF788E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA2AD188-B903-46DF-AE43-4769DBF788E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA2AD188-B903-46DF-AE43-4769DBF788E9}.Release|Any CPU.Build.0 = Release|Any CPU
{6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E007586F-9760-4744-BB25-EDEFD6BA860C} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F}
{E007586F-9760-4744-BB25-EDEFD6BA860C} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F}
{A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F}
{47EB60F8-9C89-4D13-8243-B77B01A4BA53} = {318796AF-A927-4A13-BAEE-FD13551DE91A}
{AA2AD188-B903-46DF-AE43-4769DBF788E9} = {47EB60F8-9C89-4D13-8243-B77B01A4BA53}
{6A71CD77-320C-458F-97B3-BA8F4CB530C6} = {47EB60F8-9C89-4D13-8243-B77B01A4BA53}
EndGlobalSection
EndGlobal