-
Notifications
You must be signed in to change notification settings - Fork 307
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
base: master
Are you sure you want to change the base?
Попов Захар #257
Changes from all commits
3b12dd6
cf3d993
0fddb27
335b4b0
cddc6c8
f2c3a04
29a85ee
cf7db73
a675a75
d50927d
e446905
38e8bc0
d8a9862
28e44b5
b211486
d6ec5ab
d20e854
1376542
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 |
---|---|---|
@@ -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]; | ||
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
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. Дефолтное значение удобно применять в публичных интерфейсах (например библиотек), чтобы лишний раз не заморачиваться над передачей приемлемых по умолчанию параметров. Bitmap Draw(IList<Rectangle> rectangles, int? paddingPerSide = null) Но в тестах это лишнее. Можно |
||
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; | ||
} | ||
} | ||
} |
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)); | ||
} | ||
} | ||
} |
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); | ||
} | ||
} | ||
} | ||
} |
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); | ||
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. Функция встречается трижды в коде - хороший кандидат на выделение в метод |
||
} | ||
} | ||
} |
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.
Массив байт 1000x1000 это гарантированное попадание в Large Object Heap
Большие объекты дольше создаются, дольше удаляются (причём только при сборке 2-го поколения) и занимают много места в оперативной памяти. Подробнее можно почитать по ссылке выше.
Поэтому рекомендуется по возможности их избегать.
В данном случае, что было бы, если облако тегов содержало 2 диаметрально противоположных прямоугольнка на дальних координатах?
Им может банально не хватить оперативной памяти компьютера, чтобы "закрасить" этот
grid
.Альтернативный вариант, который тут можно было реализовать: подсчёт площадей.
Мы можем легко посчитать сумму всех площадей прямоугольников: O(n) по времени, O(1) по памяти.
А также можем найти радиус описанной окружности всех прямоугольников. Что позволит посчитать площадь.
Отношение этих двух чисел будет показывать коэффициент плотности располагаемых прямоугольников.