Skip to content
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

Попов Захар #257

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3b12dd6
Черновой вариант решения второго домашнего задания.
BlizPerfect Nov 13, 2024
cf3d993
TagsCloudVisualization.csproj явно добавлен в tdd.sln.
BlizPerfect Nov 17, 2024
0fddb27
Класс CircularCloudLayouterWorker теперь является статичным.
BlizPerfect Nov 17, 2024
335b4b0
Метод Save у CircularCloudLayouterPainter теперь принимает IList<Rect…
BlizPerfect Nov 17, 2024
cddc6c8
Добавлен интерфейс ICircularCloudLayouter.
BlizPerfect Nov 17, 2024
f2c3a04
Рефакторинг класса CircularCloudLayouter.
BlizPerfect Nov 17, 2024
29a85ee
Разделён функционал отрисовки прямоугольников и сохранения изображения
BlizPerfect Nov 17, 2024
cf7db73
Добавлена проверка входных данных у CircularCloudLayouterWorker.
BlizPerfect Nov 17, 2024
a675a75
Общий рефакторинг кода.
BlizPerfect Nov 17, 2024
d50927d
Update README.md
BlizPerfect Nov 17, 2024
e446905
Update README.md
BlizPerfect Nov 17, 2024
38e8bc0
Из проекта TagsCloudVisualization удалены библиотеки для тестирования.
BlizPerfect Nov 18, 2024
d8a9862
В проекте TagsCloudVisualization явно указан атрибут видимости intern…
BlizPerfect Nov 18, 2024
28e44b5
Рефакторинг тестов на проверку основным требованиям.
BlizPerfect Nov 18, 2024
b211486
Удаление неиспользуемых using.
BlizPerfect Nov 18, 2024
d6ec5ab
CircularCloudLayouterWorker приведён в более приятный глазу вид.
BlizPerfect Nov 18, 2024
d20e854
Код приведён к единообразию использованя var.
BlizPerfect Nov 18, 2024
1376542
Merge branch 'Dev' of https://github.com/BlizPerfect/tdd into Dev
BlizPerfect Nov 18, 2024
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
53 changes: 28 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
# Test Driven Development
Примеры работ с разными параметрами:
1.
- Число прямоугольников = 100;
- Ширина прямоугольников от 30 до 70 пикселей;
- Высота прямоугольников от 20 до 50 пикселей.

Пройдя блок, ты:
![circularCloudLayouter](https://github.com/user-attachments/assets/7f83aa24-c3be-4c38-a042-6c67ea48f026)

- Узнаешь, почему полезно писать тесты вместе с кодом
- Скорее всего поймешь, что никогда раньше не писал тесты в стиле TDD по-настоящему :-)
- Получишь опыт парного TDD.
- Станешь считать стиль TDD естественным и удобным в работе
2.
- Число прямоугольников = 10000;
- Ширина прямоугольников от 30 до 70 пикселей;
- Высота прямоугольников от 20 до 50 пикселей.

![circularCloudLayouter](https://github.com/user-attachments/assets/fa3162c6-1428-465b-821e-fc748274c106)

## Необходимые знания
3.
- Число прямоугольников = 100;
- Ширина прямоугольников от 30 до 700 пикселей;
- Высота прямоугольников от 20 до 500 пикселей.

Понадобится знание C# или JS
![circularCloudLayouter](https://github.com/user-attachments/assets/5cf95ab5-fb7e-457f-8842-530b99b4be0d)

Рекомендуется пройти блок [Тестирование](https://github.com/kontur-courses/testing)
4.
- Число прямоугольников = 10;
- Ширина прямоугольников от 30 до 700 пикселей;
- Высота прямоугольников от 20 до 500 пикселей.

![circularCloudLayouter](https://github.com/user-attachments/assets/839af33d-aaba-4259-a111-2218710a45df)

## Самостоятельная подготовка
5.
- Число прямоугольников = 10;
- Ширина прямоугольников от 30 до 700 пикселей;
- Высота прямоугольников от 20 до 500 пикселей.

Посмотри [видеодемонстрацию TDD](https://www.youtube.com/watch?v=lLTv2JSrCBY) (10 минут)
![circularCloudLayouter](https://github.com/user-attachments/assets/9f0ac478-2ca5-4522-b9b1-a2764f2f4493)


## Очная встреча

~ 3.5 часа


[Инструкция для дистанционного формата](https://docs.google.com/document/d/18YYaIoyWfNzf1HXT_DCWnsqJA_3yoNkjetj_E4omMY8/edit?usp=sharing)


## Закрепление материала

1. Выполни задание [Облако тегов](HomeExercise.md)
2. Спецзадание __TDD__
Выполни ещё какую-нибудь ближайшую задачу в стиле TDD. Рабочую задачу, если уже работаешь, или учебную, если ещё учишься.
Принцип расположения прямоугольников:
Прямоугольники ставятся на окружности увеличивающегося радиуса с центром в точке, совпадающей с центром первого поставленного прямоугольника.
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using FluentAssertions;
using System.Drawing;

namespace TagsCloudVisualization.Tests
{
[TestFixture]
internal class CircularCloudLayouterMainRequirementsTests
{
private Point _center;
private Rectangle[] _rectangles;
private string _failedTestsDirectory = "FailedTest";

[OneTimeSetUp]
public void Init()
{
Directory.CreateDirectory(_failedTestsDirectory);
}

[SetUp]
public void SetUp()
{
_center = new Point(400, 400);
var minRectangleWidth = 30;
var maxRectangleWidth = 70;
var minRectangleHeight = 20;
var maxRectangleHeight = 50;
var rectanglesCount = 1000;

_rectangles = new Rectangle[rectanglesCount];
var circularCloudLayouter = new CircularCloudLayouter(_center);
for (var i = 0; i < rectanglesCount; i++)
{
var nextRectangleSize = CircularCloudLayouterWorker.GetNextRectangleSize(
minRectangleWidth,
maxRectangleWidth,
minRectangleHeight,
maxRectangleHeight);
_rectangles[i] = circularCloudLayouter.PutNextRectangle(nextRectangleSize);
}
}

[TestCase(0.7, 1000)]
[Repeat(10)]
public void ShouldPlaceRectanglesInCircle(double expectedCoverageRatio, int gridSize)
{
var maxRadius = _rectangles.Max(r => r.GetMaxDistanceFromPointToRectangleAngles(_center));
var step = (2 * maxRadius) / gridSize;

var occupancyGrid = GetOccupancyGrid(gridSize, maxRadius, step);

var actualCoverageRatio = GetOccupancyGridRatio(occupancyGrid, maxRadius, step);
actualCoverageRatio.Should().BeGreaterThanOrEqualTo(expectedCoverageRatio);
}

[TestCase(15)]
[Repeat(10)]
public void ShouldPlaceCenterOfMassOfRectanglesNearCenter(int tolerance)
{
var centerX = _rectangles.Average(r => r.Left + r.Width / 2.0);
var centerY = _rectangles.Average(r => r.Top + r.Height / 2.0);
var actualCenter = new Point((int)centerX, (int)centerY);

var distance = Math.Sqrt(Math.Pow(actualCenter.X - _center.X, 2)
+ Math.Pow(actualCenter.Y - _center.Y, 2));

distance.Should().BeLessThanOrEqualTo(tolerance);
}

[Test]
[Repeat(10)]
public void ShouldPlaceRectanglesWithoutOverlap()
{
for (var i = 0; i < _rectangles.Length; i++)
{
for (var j = i + 1; j < _rectangles.Length; j++)
{
Assert.That(
_rectangles[i].IntersectsWith(_rectangles[j]) == false,
$"Прямоугольники пересекаются:\n" +
$"{_rectangles[i].ToString()}\n" +
$"{_rectangles[j].ToString()}");
}
}
}

[TearDown]
public void Cleanup()
{
if (TestContext.CurrentContext.Result.FailCount == 0)
{
return;
}

var name = $"{TestContext.CurrentContext.Test.Name}.png";
var path = Path.Combine(_failedTestsDirectory, name);
ImageSaver.SaveFile(CircularCloudLayouterPainter.Draw(_rectangles), path);
Console.WriteLine($"Tag cloud visualization saved to file {path}");
}

[OneTimeTearDown]
public void OneTimeCleanup()
{
if (Directory.Exists(_failedTestsDirectory)
&& Directory.GetFiles(_failedTestsDirectory).Length == 0)
{
Directory.Delete(_failedTestsDirectory);
}
}

private (int start, int end) GetGridIndexesInterval(
int rectangleStartValue,
int rectangleCorrespondingSize,
double maxRadius,
double step)
{
var start = (int)((rectangleStartValue - _center.X + maxRadius) / step);
var end = (int)((rectangleStartValue + rectangleCorrespondingSize - _center.X + maxRadius) / step);
return (start, end);
}

private bool[,] GetOccupancyGrid(int gridSize, double maxRadius, double step)
{
var result = new bool[gridSize, gridSize];

Choose a reason for hiding this comment

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

Массив байт 1000x1000 это гарантированное попадание в Large Object Heap

If an object is greater than or equal to 85,000 bytes in size, it's considered a large object

Большие объекты дольше создаются, дольше удаляются (причём только при сборке 2-го поколения) и занимают много места в оперативной памяти. Подробнее можно почитать по ссылке выше.

Поэтому рекомендуется по возможности их избегать.

В данном случае, что было бы, если облако тегов содержало 2 диаметрально противоположных прямоугольнка на дальних координатах?
Им может банально не хватить оперативной памяти компьютера, чтобы "закрасить" этот grid.

Альтернативный вариант, который тут можно было реализовать: подсчёт площадей.
Мы можем легко посчитать сумму всех площадей прямоугольников: O(n) по времени, O(1) по памяти.
А также можем найти радиус описанной окружности всех прямоугольников. Что позволит посчитать площадь.
Отношение этих двух чисел будет показывать коэффициент плотности располагаемых прямоугольников.

foreach (var rect in _rectangles)
{
var xInterval = GetGridIndexesInterval(rect.X, rect.Width, maxRadius, step);
var yInterval = GetGridIndexesInterval(rect.Y, rect.Height, maxRadius, step);
for (var x = xInterval.start; x <= xInterval.end; x++)
{
for (var y = yInterval.start; y <= yInterval.end; y++)
{
result[x, y] = true;
}
}
}
return result;
}

private double GetOccupancyGridRatio(bool[,] occupancyGrid, double maxRadius, double step)
{
var totalCellsInsideCircle = 0;
var coveredCellsInsideCircle = 0;
for (var x = 0; x < occupancyGrid.GetLength(0); x++)
{
for (var y = 0; y < occupancyGrid.GetLength(0); y++)
{
var cellCenterX = x * step - maxRadius + _center.X;
var cellCenterY = y * step - maxRadius + _center.Y;

var distance = Math.Sqrt(
Math.Pow(cellCenterX - _center.X, 2) + Math.Pow(cellCenterY - _center.Y, 2));

if (distance > maxRadius)
{
continue;
}

totalCellsInsideCircle += 1;
if (occupancyGrid[x, y])
{
coveredCellsInsideCircle += 1;
}
}
}
return (double)coveredCellsInsideCircle / totalCellsInsideCircle;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Drawing;

namespace TagsCloudVisualization.Tests
{
[TestFixture]
internal class CircularCloudLayouterPainterTests
{
private int _defaultPadding = 10;

[Test]
public void Draw_ThrowsArgumentException_OnEmptyRectangleList()
{
Assert.Throws<ArgumentException>(
() => CircularCloudLayouterPainter.Draw(new List<Rectangle>()));
}

[TestCase(null, ExpectedResult = true)]
[TestCase(100, ExpectedResult = true)]
public bool Draw_CalculatesImageSizeCorrectly(int? padding)
{
var correctPadding = padding ?? _defaultPadding;
Comment on lines +17 to +21
Copy link

@Folleach Folleach Nov 18, 2024

Choose a reason for hiding this comment

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

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

Bitmap Draw(IList<Rectangle> rectangles, int? paddingPerSide = null)

Но в тестах это лишнее. Можно null в TestCase заменить на 10 - будет нагляднее

var rectangles = new List<Rectangle>()
{
new Rectangle(new Point(0, 0), new Size(10, 10))
};

var image = CircularCloudLayouterPainter.Draw(rectangles, padding);
return image.Height == rectangles[0].Height + 2 * correctPadding
&& image.Width == rectangles[0].Width + 2 * correctPadding;
}
}
}
21 changes: 21 additions & 0 deletions cs/TagsCloudVisualization.Tests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Drawing;

namespace TagsCloudVisualization.Tests
{
[TestFixture]
internal class CircularCloudLayouterTests
{
[TestCase(0, 100)]
[TestCase(-1, 100)]
[TestCase(100, 0)]
[TestCase(100, -1)]
public void PutNextRectangle_ThrowsArgumentException_OnAnyNegativeOrZeroSize(
int width,
int height)
{
var size = new Size(width, height);
Assert.Throws<ArgumentException>(
() => new CircularCloudLayouter(new Point()).PutNextRectangle(size));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace TagsCloudVisualization.Tests
{
[TestFixture]
internal class CircularCloudLayouterWorkerTests
{
[TestCase(0, 100)]
[TestCase(-1, 100)]
[TestCase(100, 0)]
[TestCase(100, -1)]
public void GetNextRectangleSize_ThrowsArgumentException_OnAnyNegativeOrZeroSize(int width, int height)
{
Assert.Throws<ArgumentException>(
() => CircularCloudLayouterWorker.GetNextRectangleSize(width, width, height, height));
}

[TestCase(50, 25, 25, 50)]
[TestCase(25, 50, 50, 25)]
public void GetNextRectangleSize_ThrowsArgumentException_OnNonConsecutiveSizeValues(
int minWidth,
int maxWidth,
int minHeight,
int maxHeight)
{
Assert.Throws<ArgumentException>(
() => CircularCloudLayouterWorker.GetNextRectangleSize(minWidth, maxWidth, minHeight, maxHeight));
}
}
}
53 changes: 53 additions & 0 deletions cs/TagsCloudVisualization.Tests/ImageSaverTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Drawing;

namespace TagsCloudVisualization.Tests
{
[TestFixture]
internal class ImageSaverTest
{
private string _directoryPath = "TempFilesForTests";

[OneTimeSetUp]
public void Init()
{
Directory.CreateDirectory(_directoryPath);
}

[TestCase("Test.png")]
public void SaveFile_ArgumentNullException_WithNullBitmap(string filename)
{
var path = Path.Combine(_directoryPath, filename);
Assert.Throws<ArgumentNullException>(() => ImageSaver.SaveFile(null, path));
}

[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void SaveFile_ThrowsArgumentException_WithInvalidFilename(string? filename)
{
var dummyImage = new Bitmap(1, 1);
Assert.Throws<ArgumentException>(() => ImageSaver.SaveFile(dummyImage, filename));
}

[TestCase("Test.png", ExpectedResult = true)]
public bool SaveFile_SavesFile(string filename)
{
var dummyImage = new Bitmap(1, 1);
var path = Path.Combine(_directoryPath, filename);

File.Delete(path);
ImageSaver.SaveFile(dummyImage, path);
return File.Exists(path);
}


[OneTimeTearDown]
public void OneTimeCleanup()
{
if (Directory.Exists(_directoryPath))
{
Directory.Delete(_directoryPath, true);
}
}
}
}
16 changes: 16 additions & 0 deletions cs/TagsCloudVisualization.Tests/RectangleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Drawing;

namespace TagsCloudVisualization.Tests
{
internal static class RectangleExtensions
{
public static double GetMaxDistanceFromPointToRectangleAngles(this Rectangle rectangle, Point point)
{
var dx = Math.Max(
Math.Abs(rectangle.X - point.X), Math.Abs(rectangle.X + rectangle.Width - point.X));
var dy = Math.Max(
Math.Abs(rectangle.Y - point.Y), Math.Abs(rectangle.Y + rectangle.Height - point.Y));
return Math.Sqrt(dx * dx + dy * dy);

Choose a reason for hiding this comment

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

Функция встречается трижды в коде - хороший кандидат на выделение в метод

}
}
}
Loading