From 12c1af2ee39fa3fa50c88d6852d364736ef2ea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jone=C5=A1?= Date: Fri, 9 Dec 2022 13:49:25 +0100 Subject: [PATCH 1/2] Document testing --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 81b6605..3b90e07 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ This library is also used in production by [KnowledgePicker](https://knowledgepi As mentioned [above](#how-to-use), only subset of functionality is implemented now, but all contributions are welcome. Feel free to open [issues](https://github.com/knowledgepicker/word-cloud/issues) and [pull requests](https://github.com/knowledgepicker/word-cloud/pulls). +### Testing + +Tests are currently only supported on Linux, because they are snapshot tests (generating a word cloud image and comparing it byte-by-byte with a snapshot) and more work is needed to ensure this is cross-platform (e.g., use exactly the same font). On Windows, tests can be run in WSL (Visual Studio supports this directly). Tests are also automatically run in GitHub Actions. + ### Release process After pushing a tag, GitHub workflow `release.yml` is triggered which builds and publishes the NuGet package. From a901809f71b0106e4f9a73d59d2501a6ae483aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jone=C5=A1?= Date: Sat, 10 Dec 2022 12:28:13 +0100 Subject: [PATCH 2/2] Avoid state sharing --- .../Drawing/IGraphicEngine.cs | 2 + .../Drawing/SkGraphicEngine.cs | 15 +++++++ .../Layouts/BaseLayout.cs | 2 + .../Layouts/ILayout.cs | 1 + .../Layouts/SpiralLayout.cs | 5 +++ .../Primitives/LayoutItem.cs | 2 +- .../WordCloudGenerator.cs | 10 ++++- .../WordCloudGeneratorTests.cs | 39 +++++++++++++++++++ 8 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 test/KnowledgePicker.WordCloud.Tests/WordCloudGeneratorTests.cs diff --git a/src/KnowledgePicker.WordCloud/Drawing/IGraphicEngine.cs b/src/KnowledgePicker.WordCloud/Drawing/IGraphicEngine.cs index 8791d6c..bfee1bb 100644 --- a/src/KnowledgePicker.WordCloud/Drawing/IGraphicEngine.cs +++ b/src/KnowledgePicker.WordCloud/Drawing/IGraphicEngine.cs @@ -35,5 +35,7 @@ public interface IGraphicEngine : IDisposable public interface IGraphicEngine : IGraphicEngine { TBitmap Bitmap { get; } + + IGraphicEngine Clone(); } } diff --git a/src/KnowledgePicker.WordCloud/Drawing/SkGraphicEngine.cs b/src/KnowledgePicker.WordCloud/Drawing/SkGraphicEngine.cs index 4c0b721..0dbbbd6 100644 --- a/src/KnowledgePicker.WordCloud/Drawing/SkGraphicEngine.cs +++ b/src/KnowledgePicker.WordCloud/Drawing/SkGraphicEngine.cs @@ -13,6 +13,16 @@ public sealed class SkGraphicEngine : IGraphicEngine private readonly SKPaint textPaint; private readonly WordCloudInput wordCloud; + private SkGraphicEngine(ISizer sizer, WordCloudInput wordCloud, + SKPaint textPaint) + { + Sizer = sizer; + this.wordCloud = wordCloud; + this.textPaint = textPaint; + Bitmap = new SKBitmap(wordCloud.Width, wordCloud.Height); + canvas = new SKCanvas(Bitmap); + } + public SkGraphicEngine(ISizer sizer, WordCloudInput wordCloud, SKTypeface? font = null, bool antialias = true) { @@ -52,6 +62,11 @@ public void Draw(PointD location, RectangleD measured, string text, int count, s (float)(location.Y - measured.Top), textPaint); } + public IGraphicEngine Clone() + { + return new SkGraphicEngine(Sizer, wordCloud, textPaint); + } + public void Dispose() { textPaint.Dispose(); diff --git a/src/KnowledgePicker.WordCloud/Layouts/BaseLayout.cs b/src/KnowledgePicker.WordCloud/Layouts/BaseLayout.cs index cb42515..5ff5190 100644 --- a/src/KnowledgePicker.WordCloud/Layouts/BaseLayout.cs +++ b/src/KnowledgePicker.WordCloud/Layouts/BaseLayout.cs @@ -39,6 +39,8 @@ public IEnumerable GetWordsInArea(RectangleD area) return QuadTree.Query(area); } + public abstract ILayout Clone(); + protected bool IsInsideSurface(RectangleD targetRectangle) { return IsInside(Surface, targetRectangle); diff --git a/src/KnowledgePicker.WordCloud/Layouts/ILayout.cs b/src/KnowledgePicker.WordCloud/Layouts/ILayout.cs index 624ae7e..065b92a 100644 --- a/src/KnowledgePicker.WordCloud/Layouts/ILayout.cs +++ b/src/KnowledgePicker.WordCloud/Layouts/ILayout.cs @@ -11,5 +11,6 @@ public interface ILayout { int Arrange(IEnumerable entries, IGraphicEngine engine); IEnumerable GetWordsInArea(RectangleD area); + ILayout Clone(); } } diff --git a/src/KnowledgePicker.WordCloud/Layouts/SpiralLayout.cs b/src/KnowledgePicker.WordCloud/Layouts/SpiralLayout.cs index a7a89a6..137a59b 100644 --- a/src/KnowledgePicker.WordCloud/Layouts/SpiralLayout.cs +++ b/src/KnowledgePicker.WordCloud/Layouts/SpiralLayout.cs @@ -40,6 +40,11 @@ public override bool TryFindFreeRectangle(SizeD size, out RectangleD foundRectan return false; } + public override ILayout Clone() + { + return new SpiralLayout(WordCloud); + } + private static double GetPseudoRandomStartAngle(SizeD size) { return size.Height * size.Width; diff --git a/src/KnowledgePicker.WordCloud/Primitives/LayoutItem.cs b/src/KnowledgePicker.WordCloud/Primitives/LayoutItem.cs index 13d1271..9e04331 100644 --- a/src/KnowledgePicker.WordCloud/Primitives/LayoutItem.cs +++ b/src/KnowledgePicker.WordCloud/Primitives/LayoutItem.cs @@ -3,7 +3,7 @@ namespace KnowledgePicker.WordCloud.Primitives /// /// Word arranged somewhere in word cloud. /// - public class LayoutItem + public record LayoutItem { public LayoutItem(WordCloudEntry entry, PointD location, RectangleD measured) { diff --git a/src/KnowledgePicker.WordCloud/WordCloudGenerator.cs b/src/KnowledgePicker.WordCloud/WordCloudGenerator.cs index f393df9..0a7724e 100644 --- a/src/KnowledgePicker.WordCloud/WordCloudGenerator.cs +++ b/src/KnowledgePicker.WordCloud/WordCloudGenerator.cs @@ -41,13 +41,19 @@ public WordCloudGenerator(WordCloudInput wordCloud, private T Process( Func, IEnumerable, T> handler) { + // Ensure state is not shared. + // TODO: We should instead use factory pattern. + // But that would be a big change in usage of this class. + var localEngine = engine.Clone(); + var localLayout = layout.Clone(); + // Arrange word cloud. var size = new SizeD(wordCloud.Width, wordCloud.Height); - layout.Arrange(wordCloud.Entries, engine); + localLayout.Arrange(wordCloud.Entries, localEngine); // Process results. var area = new RectangleD(new PointD(0, 0), size); - return handler(engine, layout.GetWordsInArea(area)); + return handler(localEngine, localLayout.GetWordsInArea(area)); } public IEnumerable<(LayoutItem Item, double FontSize)> Arrange() diff --git a/test/KnowledgePicker.WordCloud.Tests/WordCloudGeneratorTests.cs b/test/KnowledgePicker.WordCloud.Tests/WordCloudGeneratorTests.cs new file mode 100644 index 0000000..b4d4440 --- /dev/null +++ b/test/KnowledgePicker.WordCloud.Tests/WordCloudGeneratorTests.cs @@ -0,0 +1,39 @@ +using KnowledgePicker.WordCloud.Drawing; +using KnowledgePicker.WordCloud.Layouts; +using KnowledgePicker.WordCloud.Primitives; +using KnowledgePicker.WordCloud.Sizers; +using SkiaSharp; + +namespace KnowledgePicker.WordCloud.Tests; + +public class WordCloudGeneratorTests +{ + [Fact] // https://github.com/knowledgepicker/word-cloud/issues/17 + public void DoesNotShareState() + { + // Arrange. + var wordCloud = new WordCloudInput(new[] + { + new WordCloudEntry("a", 1), + new WordCloudEntry("b", 1), + }) + { + Width = 1024, + Height = 256, + MinFontSize = 8, + MaxFontSize = 32 + }; + var sizer = new LogSizer(wordCloud); + using var engine = new SkGraphicEngine(sizer, wordCloud); + var layout = new SpiralLayout(wordCloud); + var wcg = new WordCloudGenerator(wordCloud, engine, layout); + + // Act. + var result1 = wcg.Arrange().ToArray(); + var result2 = wcg.Arrange().ToArray(); + + // Assert. + Assert.Equal(result1.AsEnumerable(), result2); + Assert.Equal(2, result2.Length); + } +}