using System; using Prism; using System.Globalization; using System.Threading; using Amphora; using Glass.Text; using UnitTestSharp; using static Glass.Text.Styles; namespace Glass.UnitTests.Text { public class TextPiecesTests : TestFixture { public static string Render(in TextPieces line) { return line.ToString(); } public static string DescribeSpans(in TextPieces line) { char[] scratch = null; var spans = new DynamicArray(); line.ViewIn(NumberFormat.Invariant, ref scratch, spans); string returnMe = ""; for (int i = 0; i < spans.Count; ++i) { returnMe += spans[i].Start + "-" + spans[i].End + ":" + (spans[i].Style.TintColor?.ToArgb() & 0xFFFFFF)?.ToString("X6") + " "; } return returnMe.TrimEnd(); } public static string Fold(string literal) { var line = new TextPieces(0, 0); line.AppendLiteral(literal); return line.ToString(); } public static string NumberText(long value) { var line = new TextPieces(0, 1); line.AppendFormatted(Plain(value)); return line.ToString(); } public static string CountText(long value, NumberFormat format) { var line = new TextPieces(0, 1); line.AppendFormatted(Count(value)); char[] scratch = null; return line.ViewIn(format, ref scratch).ToString(); } public static readonly NumberFormat Indian = NumberFormatTests.Grouping(",", 3, 2, "-"); public static readonly NumberFormat Spaced = NumberFormatTests.Grouping(new string((char)0x202F, 1), 3, 3, new string((char)0x2212, 1)); public class CtorTests : TestFixture { public void StartsWithNoSegments() { var line = new TextPieces(0, 0); CheckEqual(0, line.SegmentCount); } public void SizesFromTheCompilerChangeNothing() { var line = new TextPieces(literalLength: 500, formattedCount: 500); CheckEqual(0, line.SegmentCount); } } public class AppendLiteralTests : TestFixture { public void PlainText_IsKept() { CheckEqual("Hello, world.", Fold("Hello, world.")); } public void EmptyText_AddsNoSegment() { var line = new TextPieces(0, 0); line.AppendLiteral(""); CheckEqual(0, line.SegmentCount); } public void NonEmptyText_AddsOneSegment() { var line = new TextPieces(0, 0); line.AppendLiteral("a"); CheckEqual(1, line.SegmentCount); } public void SingleCrLf_BecomesOneSpace() { CheckEqual("a b", Fold("a\r\nb")); } public void SingleLf_BecomesOneSpace() { CheckEqual("a b", Fold("a\nb")); } public void SingleLoneCr_BecomesOneSpace() { CheckEqual("a b", Fold("a\rb")); } public void IndentationAfterALineBreak_FoldsIntoTheSpace() { CheckEqual("a b", Fold("a\n b")); } public void TrailingSpacesBeforeALineBreak_FoldIntoTheSpace() { CheckEqual("a b", Fold("a \n b")); } public void BlankLine_BecomesAParagraphBreak() { CheckEqual("One.\nTwo.", Fold("One.\n\n Two.")); } public void LineWithOnlyWhitespace_BecomesAParagraphBreak() { CheckEqual("One.\nTwo.", Fold("One.\r\n \r\nTwo.")); } public void ManyBlankLines_BecomeOneParagraphBreak() { CheckEqual("a\nb", Fold("a\n\n\n\nb")); } public void LfThenCr_CountsAsTwoLineBreaks() { CheckEqual("a\nb", Fold("a\n\rb")); } public void TwoCrLfs_CountAsTwoLineBreaks() { CheckEqual("a\nb", Fold("a\r\n\r\nb")); } public void CrAtTheEnd_CountsAsALineBreak() { CheckEqual("a ", Fold("a\r")); } public void LineBreakAtTheStart_BecomesASpace() { CheckEqual(" a", Fold("\n a")); } public void LineBreakAtTheEnd_BecomesASpace() { CheckEqual("a ", Fold("a\n")); } public void RunOfSpacesWithoutALineBreak_IsKept() { CheckEqual("a b", Fold("a b")); } public void TabsWithoutALineBreak_AreKept() { CheckEqual("a\t\tb", Fold("a\t\tb")); } public void MixedSpacesAndTabsWithoutALineBreak_AreKept() { CheckEqual("a \t b", Fold("a \t b")); } public void NonWhitespaceCharacters_PassThroughUnchanged() { string text = "café 漢字 " + char.ConvertFromUtf32(0x1F600) + " ­"; CheckEqual(text, Fold(text)); } public void EachLiteralFoldsOnItsOwn() { var line = new TextPieces(0, 0); line.AppendLiteral("a\n"); line.AppendLiteral("\nb"); CheckEqual("a b", line.ToString()); } public void RawStringLiteral_FoldsItsMarginsAndLineBreaks() { string name = "Ada"; int count = 7; CheckEqual("Hello Ada, you just missed 7.", Render($""" Hello {name}, you just missed {Plain(count)}. """)); } public void LineBreakBeforeAHole_BecomesASpaceBeforeIt() { string name = "Ada"; CheckEqual("Hi Ada", Render($"Hi\n {name}")); } } public class AppendFormattedStringTests : TestFixture { public void String_IsInserted() { string name = "Ada"; CheckEqual("Hello Ada!", Render($"Hello {name}!")); } public void NullString_IsEmpty() { CheckEqual("[]", Render($"[{(string)null}]")); } public void EmptyString_StillAddsASegment() { var line = new TextPieces(0, 1); line.AppendFormatted(""); CheckEqual(1, line.SegmentCount); } public void NullString_StillAddsASegment() { var line = new TextPieces(0, 1); line.AppendFormatted((string)null); CheckEqual(1, line.SegmentCount); } public void LineBreaksAndIndentationInTheString_AreKept() { string poem = "roses\n are red"; CheckEqual("roses\n are red", Render($"{poem}")); } public void StringWithBracesAndPercentSigns_IsKept() { string text = "{0} 100% {name}"; CheckEqual("{0} 100% {name}", Render($"{text}")); } public void ThirtyTwoStrings_Fit() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.TextCapacity; ++i) { line.AppendFormatted("s"); } CheckEqual(new string('s', TextPieces.TextCapacity), line.ToString()); } public void OneMoreThanTheTextCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.TextCapacity; ++i) { line.AppendFormatted("s"); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted("s"); } public void LiteralsAndStrings_ShareTheTextCapacity() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.TextCapacity / 2; ++i) { line.AppendLiteral("a"); line.AppendFormatted("s"); } CheckThrow(typeof(InvalidOperationException)); line.AppendLiteral("a"); } } public class AppendFormattedNumberTests : TestFixture { public void Zero_IsOneDigit() { CheckEqual("0", NumberText(0)); } public void SingleDigit_IsThatDigit() { CheckEqual("7", NumberText(7)); } public void Positive_HasNoSign() { CheckEqual("1234567", NumberText(1234567)); } public void Negative_HasAMinusSign() { CheckEqual("-42", NumberText(-42)); } public void NegativeOne_HasAMinusSign() { CheckEqual("-1", NumberText(-1)); } public void MaxValue_IsAllItsDigits() { CheckEqual("9223372036854775807", NumberText(long.MaxValue)); } public void MinValue_IsAllItsDigits() { CheckEqual("-9223372036854775808", NumberText(long.MinValue)); } public void Int_ConvertsToALong() { int count = 7; CheckEqual("You have 7 muffins.", Render($"You have {Plain(count)} muffins.")); } public void SmallerIntegerTypes_ConvertToALong() { byte small = 200; short shorter = -300; uint unsigned = 4000000000; CheckEqual("200 -300 4000000000", Render($"{Plain(small)} {Plain(shorter)} {Plain(unsigned)}")); } public void Plain_IgnoresTheFormat() { var line = new TextPieces(0, 1); line.AppendFormatted(Plain(-1234567)); char[] scratch = null; CheckEqual("-1234567", line.ViewIn(Spaced, ref scratch).ToString()); } public void Plain_IgnoresTheThreadsCulture() { CultureInfo original = Thread.CurrentThread.CurrentCulture; try { Thread.CurrentThread.CurrentCulture = new CultureInfo("ar-SA"); CheckEqual("-1234567", NumberText(-1234567)); } finally { Thread.CurrentThread.CurrentCulture = original; } } public void Count_GroupsDigitsInTheFormat() { CheckEqual("1,234,567", CountText(1234567, NumberFormat.Invariant)); CheckEqual("12,34,567", CountText(1234567, Indian)); CheckEqual("1" + (char)0x202F + "234" + (char)0x202F + "567", CountText(1234567, Spaced)); } public void Count_WritesTheFormatsMinusSign() { CheckEqual((char)0x2212 + "1" + (char)0x202F + "000", CountText(-1000, Spaced)); } public void Count_OfASmallNumber_HasNoSeparator() { CheckEqual("999", CountText(999, NumberFormat.Invariant)); } public void Count_OfTheLongExtremes_FitsTheBuffer() { CheckEqual("9,223,372,036,854,775,807", CountText(long.MaxValue, NumberFormat.Invariant)); CheckEqual("-9,223,372,036,854,775,808", CountText(long.MinValue, NumberFormat.Invariant)); } public void Count_IgnoresTheThreadsCulture() { CultureInfo original = Thread.CurrentThread.CurrentCulture; try { Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); CheckEqual("1,234", CountText(1234, NumberFormat.Invariant)); } finally { Thread.CurrentThread.CurrentCulture = original; } } public void Count_ToStringUsesTheInvariantFormat() { CheckEqual("Got 1,234.", Render($"Got {Count(1234)}.")); } public void CountsAndPlainNumbers_ShareTheNumberCapacity() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity / 2; ++i) { line.AppendFormatted(Count(1)); line.AppendFormatted(Plain(1)); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Count(1)); } public void StyledCount_KeepsItsStyleAndKind() { var line = new TextPieces(0, 1); line.AppendFormatted(Tint(Colors.Red, Count(1234))); CheckEqual("1,234", line.ToString()); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(0)); } public void NumberHole_AddsOneSegment() { var line = new TextPieces(0, 1); line.AppendFormatted(Plain(5L)); CheckEqual(1, line.SegmentCount); } public void SixteenNumbers_Fit() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Plain(1L)); } CheckEqual(new string('1', TextPieces.NumberCapacity), line.ToString()); } public void OneMoreThanTheNumberCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Plain(1L)); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Plain(1L)); } public void EveryNumberSlotAndTextSlot_CanBeUsedTogether() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Plain(i)); } for (int i = 0; i < TextPieces.TextCapacity; ++i) { line.AppendFormatted("x"); } CheckEqual(TextPieces.NumberCapacity + TextPieces.TextCapacity, line.SegmentCount); CheckEqual("0123456789101112131415" + new string('x', TextPieces.TextCapacity), line.ToString()); } } public class AppendFormattedFixedTests : TestFixture { public static readonly NumberFormat German = NumberFormat.FromCulture(new CultureInfo("de-DE")); [IgnoreTest] public string In(NumberFormat format, in TextPieces line) { char[] scratch = null; return line.ViewIn(format, ref scratch).ToString(); } public void ADouble_ReadsWithItsDecimalsAndGroups() { double price = 1234.5; CheckEqual("Cost 1,234.50.", Render($"Cost {Fixed(price, 2)}.")); } public void TheFormatsSeparators_AreUsed() { double price = 1234.5; CheckEqual("Cost 1.234,50.", In(German, $"Cost {Fixed(price, 2)}.")); } public void EachHole_KeepsItsOwnDecimals() { CheckEqual("1.00 1 1.000", Render($"{Fixed(1, 2)} {Fixed(1, 0)} {Fixed(1, 3)}")); } public void EveryNumberSlot_KeepsItsOwnDecimals() { var line = new TextPieces(0, 0); string expected = ""; for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Fixed(1, i % 10)); line.AppendLiteral(" "); expected += "1" + (i % 10 == 0 ? "" : "." + new string('0', i % 10)) + " "; } CheckEqual(expected, line.ToString()); } public void ARoundedValue_ReadsRounded() { CheckEqual("1.01 -2.5 0.00", Render($"{Fixed(1.005, 2)} {Fixed(-2.45, 1)} {Fixed(-0.001, 2)}")); } public void NaN_ReadsAsTheFormatsSymbol() { CheckEqual("NaN", Render($"{Fixed(double.NaN, 2)}")); CheckEqual("?", In(new NumberFormat(",", 3, 3, "-", ".", "?", "inf", "e", ShortNotation.English), $"{Fixed(double.NaN, 2)}")); } public void Infinities_ReadAsTheFormatsSymbol() { var format = new NumberFormat(",", 3, 3, "~", ".", "?", "inf", "e", ShortNotation.English); CheckEqual("Infinity -Infinity", Render($"{Fixed(double.PositiveInfinity, 2)} {Fixed(double.NegativeInfinity, 2)}")); CheckEqual("inf ~inf", In(format, $"{Fixed(double.PositiveInfinity, 2)} {Fixed(double.NegativeInfinity, 2)}")); } public void ALargeValueWithManyDecimals_ReadsInFull() { CheckEqual("10,000,000,000.000000000", Render($"{Fixed(1e10, 9)}")); } public void ABufferSizedForAFixedNumber_FitsTheLongestOne() { var wide = new NumberFormat("<->", 1, 1, "MINUS", "", "NaN", "Infinity", "e", ShortNotation.English); var line = new TextPieces(0, 1); line.AppendFormatted(Fixed(-7.8e28, 9)); char[] scratch = null; line.ViewIn(wide, ref scratch); CheckTrue(scratch.Length >= 29 + 28 * 3 + 5 + 5 + 9); } public void ANumberPastTheNumberCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Fixed(1, 2)); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Fixed(1, 2)); } public void AStyledFixed_KeepsItsStyleAndDecimals() { var line = new TextPieces(0, 1); line.AppendFormatted(Tint(Colors.Red, Fixed(2.5, 2))); CheckEqual("2.50", line.ToString()); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(0)); } public void AStyledSymbol_HasASpanOverIt() { CheckEqual("2-5:FF0000", DescribeSpans($"x {Tint(Colors.Red, Fixed(double.NaN, 2))}")); CheckEqual("2-10:FF0000", DescribeSpans($"x {Tint(Colors.Red, Fixed(double.PositiveInfinity, 2))}")); } public void ABufferForALongSeparatorFormat_FitsTheLongestFixedNumber() { var wide = new NumberFormat("<->", 1, 1, "MINUS", "", "NaN", "Infinity", "e", ShortNotation.English); CheckEqual("MINUS9<->2<->2<->3<->3<->7<->2<->0<->3<->6855", In(wide, $"{Fixed(-9223372036.8548, 3)}")); } } public class AppendFormattedScientificTests : TestFixture { [IgnoreTest] public string In(NumberFormat format, in TextPieces line) { char[] scratch = null; return line.ViewIn(format, ref scratch).ToString(); } public void ADouble_ReadsAsAMantissaAndAnExponent() { double gold = 1.2345e45; CheckEqual("You have 1.23e45 gold.", Render($"You have {Scientific(gold, 3)} gold.")); } public void TheFormatsSeparatorsAndSymbols_AreUsed() { var format = new NumberFormat(".", 3, 3, "-", ",", "NaN", "Infinity", "E", ShortNotation.English); CheckEqual("1,23E45", In(format, $"{Scientific(1.2345e45, 3)}")); } public void EachHole_KeepsItsOwnDigits() { CheckEqual("1.2e3 1.235e3 1e3", Render($"{Scientific(1234.5, 2)} {Scientific(1234.5, 4)} {Scientific(1234.5, 1)}")); } public void ScientificAndFixedHoles_KeepTheirOwnKindsAndPrecisions() { CheckEqual("1,234.50 1.23e3 1,234.5", Render($"{Fixed(1234.5, 2)} {Scientific(1234.5, 3)} {Fixed(1234.5, 1)}")); } public void EveryNumberSlot_KeepsItsOwnDigits() { var line = new TextPieces(0, 0); string expected = ""; for (int i = 0; i < TextPieces.NumberCapacity; ++i) { int digits = i % 15 + 1; line.AppendFormatted(Scientific(1, digits)); line.AppendLiteral(" "); expected += "1" + (digits == 1 ? "" : "." + new string('0', digits - 1)) + "e0 "; } CheckEqual(expected, line.ToString()); } public void NaNAndInfinities_ReadAsTheFormatsSymbols() { double nan = double.NaN; double up = double.PositiveInfinity; double down = double.NegativeInfinity; CheckEqual("NaN Infinity -Infinity", Render($"{Scientific(nan, 3)} {Scientific(up, 3)} {Scientific(down, 3)}")); } public void AStyledScientific_KeepsItsStyleAndSpan() { CheckEqual("2-9:FF0000", DescribeSpans($"x {Tint(Colors.Red, Scientific(1.5e30, 3))}")); } public void ANumberPastTheNumberCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Scientific(1, 3)); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Scientific(1, 3)); } public void ABufferSizedForAScientificNumber_FitsTheLongestOne() { var wide = new NumberFormat(",", 3, 3, "MINUS", "", "NaN", "Infinity", "EXP", ShortNotation.English); CheckEqual("MINUS123456789012345EXPMINUS300", In(wide, $"{Scientific(-1.23456789012345e-300, 15)}")); } } public class AppendFormattedShortExponentTests : TestFixture { [IgnoreTest] public string In(NumberFormat format, in TextPieces line) { char[] scratch = null; return line.ViewIn(format, ref scratch).ToString(); } public void ADouble_ReadsWithASuffix() { double gold = 1234567; CheckEqual("You have 1.23M gold.", Render($"You have {ShortExponent(gold, 3)} gold.")); } public void TheFormatsSeparatorsAndSuffixes_AreUsed() { var notation = new ShortNotation(new[] { " Tsd" }, "", 3); var format = new NumberFormat(".", 3, 3, "-", ",", "NaN", "Infinity", "e", notation); CheckEqual("1,23 Tsd", In(format, $"{ShortExponent(1234, 3)}")); } public void EachHole_KeepsItsOwnDigits() { CheckEqual("1.2K 1.235K 1K", Render($"{ShortExponent(1234.5, 2)} {ShortExponent(1234.5, 4)} {ShortExponent(1234.5, 1)}")); } public void AllTheNumberKinds_KeepTheirOwnKindsAndPrecisions() { CheckEqual("1,234.50 1.23e3 1.23K 1,235", Render($"{Fixed(1234.5, 2)} {Scientific(1234.5, 3)} {ShortExponent(1234.5, 3)} {Count(1235)}")); } public void EveryNumberSlot_KeepsItsOwnDigits() { var line = new TextPieces(0, 0); string expected = ""; for (int i = 0; i < TextPieces.NumberCapacity; ++i) { int digits = i % 15 + 1; line.AppendFormatted(ShortExponent(1000, digits)); line.AppendLiteral(" "); expected += (digits == 1 ? "1" : "1." + new string('0', digits - 1)) + "K "; } CheckEqual(expected, line.ToString()); } public void NaNAndInfinities_ReadAsTheFormatsSymbols() { double nan = double.NaN; double up = double.PositiveInfinity; double down = double.NegativeInfinity; CheckEqual("NaN Infinity -Infinity", Render($"{ShortExponent(nan, 3)} {ShortExponent(up, 3)} {ShortExponent(down, 3)}")); } public void AStyledShortExponent_KeepsItsStyleAndSpan() { CheckEqual("2-8:FF0000", DescribeSpans($"x {Tint(Colors.Red, ShortExponent(1.5e30, 3))}")); } public void ANumberPastTheNumberCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(ShortExponent(1, 3)); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(ShortExponent(1, 3)); } public void ABufferSizedForAShortExponent_FitsTheLongestOne() { var wide = new NumberFormat(",", 3, 3, "MINUS", "", "NaN", "Infinity", "EXP", ShortNotation.English); CheckEqual("MINUS123456789012345dt", In(wide, $"{ShortExponent(-1.23456789012345e308, 15)}")); } } public class IsBuiltTests : TestFixture { public void ADefaultLine_IsNotBuilt() { CheckFalse(default(TextPieces).IsBuilt); } public void AnEmptyInterpolatedString_IsBuilt() { TextPieces line = $""; CheckTrue(line.IsBuilt); } public void ALineMadeByTheConstructor_IsBuilt() { CheckTrue(new TextPieces(0, 0).IsBuilt); } public void AVariantMadeWithASelector_IsBuilt() { CheckTrue(new TextPieces(0, 0, Count(1)).IsBuilt); } } public class SelectorCtorTests : TestFixture { public void AStringSelector_Throws() { CheckThrow(typeof(ArgumentException)); new TextPieces(0, 0, "three"); } public void AnAmountSelector_Throws() { CheckThrow(typeof(ArgumentException)); new TextPieces(0, 0, Plurals.Amount); } public void AnyKindOfNumber_IsAccepted() { CheckEqual("1.50", AmountOf(Fixed(1.5, 2))); CheckEqual("1,234", AmountOf(Count(1234))); CheckEqual("1234", AmountOf(Plain(1234))); CheckEqual("1.23e3", AmountOf(Scientific(1234, 3))); CheckEqual("1.23K", AmountOf(ShortExponent(1234, 3))); } [IgnoreTest] public string AmountOf(StyledText selector) { var line = new TextPieces(0, 1, selector); line.AppendFormatted(Plurals.Amount); return line.ToString(); } } public class AmountTests : TestFixture { public void WithoutASelector_Throws() { var line = new TextPieces(0, 1); CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Plurals.Amount); } public void IsTheSelectorWrittenInItsOwnKind() { var line = new TextPieces(0, 3, Fixed(1234.5, 1)); line.AppendFormatted(Plurals.Amount); line.AppendLiteral(" or "); line.AppendFormatted(Plurals.Amount); CheckEqual("1,234.5 or 1,234.5", line.ToString()); } public void CanBeWrappedInALook() { var line = new TextPieces(0, 1, Count(5)); line.AppendFormatted(Tint(Colors.Gold, Plurals.Amount)); CheckEqual("5", line.ToString()); CheckEqual(new TextStyle { TintColor = Colors.Gold }, line.StyleOfSegment(0)); } public void AnUnwrappedAmount_HasNoLook() { var line = new TextPieces(0, 1, Count(5)); line.AppendFormatted(Plurals.Amount); CheckNull(line.StyleOfSegment(0)); } public void TheSelectorsOwnLook_IsKept() { var line = new TextPieces(0, 1, Tint(Colors.Red, Count(5))); line.AppendFormatted(Plurals.Amount); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(0)); } public void TheSelectorsOwnLook_WinsOverOneLaidOnAmount() { var line = new TextPieces(0, 1, Tint(Colors.Red, Count(5))); line.AppendFormatted(Tint(Colors.Gold, Gamma(2, Plurals.Amount))); CheckEqual(new TextStyle { TintColor = Colors.Red, CoverageGamma = 2 }, line.StyleOfSegment(0)); } public void EachUse_TakesANumberSlot() { var line = new TextPieces(0, 0, Count(5)); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Plurals.Amount); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Plurals.Amount); } public void ANegativeSelector_ShowsItsSign() { var line = new TextPieces(0, 1, Count(-5)); line.AppendFormatted(Plurals.Amount); CheckEqual("-5", line.ToString()); } } public class SegmentCountTests : TestFixture { public void CountsLiteralsAndHoles() { string name = "Ada"; int count = 3; var line = new TextPieces(0, 0); line.AppendLiteral("Hello "); line.AppendFormatted(name); line.AppendLiteral(", you have "); line.AppendFormatted(Plain(count)); line.AppendLiteral(" things"); CheckEqual(5, line.SegmentCount); } } public class ViewInTests : TestFixture { private char[] scratch; [IgnoreTest] public int LengthOfTheView(in TextPieces line) { StringView view = line.ViewIn(NumberFormat.Invariant, ref scratch); return view.EndIndex - view.StartIndex; } [UnitTestSharp.AllocationTest] public void BuildingAndReadingALine_AllocatesNothingOnceTheScratchHasGrown() { string name = "Ada"; int count = 7; int length = LengthOfTheView( $"Hello {name}, you have {Plain(count)} muffins and {Count(-1234567890123L)} dust."); CheckNoAllocations(); CheckEqual("Hello Ada, you have 7 muffins and -1,234,567,890,123 dust.".Length, length); } [UnitTestSharp.AllocationTest] public void ReadingFixedNumbers_AllocatesNothingOnceTheScratchHasGrown() { double price = 1234.5678; int length = LengthOfTheView($"{Fixed(price, 2)} {Fixed(-1e25, 9)} {Fixed(double.NaN, 1)}"); CheckNoAllocations(); CheckEqual("1,234.57 -10,000,000,000,000,000,000,000,000.000000000 NaN".Length, length); } [UnitTestSharp.AllocationTest] public void ReadingScientificNumbers_AllocatesNothingOnceTheScratchHasGrown() { double gold = 1.2345e300; int length = LengthOfTheView($"{Scientific(gold, 4)} {Scientific(-1e-300, 15)} {Scientific(0, 2)}"); CheckNoAllocations(); CheckEqual("1.235e300 -1.00000000000000e-300 0.0e0".Length, length); } [UnitTestSharp.AllocationTest] public void ReadingShortExponentNumbers_AllocatesNothingOnceTheScratchHasGrown() { double gold = 1.2345e300; int length = LengthOfTheView( $"{ShortExponent(gold, 4)} {ShortExponent(0.0123, 3)} {ShortExponent(-5e18, 2)}"); CheckNoAllocations(); CheckEqual("1.235dr 0.0123 -5.0ab".Length, length); } public void View_LooksIntoTheScratchFromTheStart() { char[] buffer = null; var line = new TextPieces(0, 0); line.AppendLiteral("abc"); StringView view = line.ViewIn(NumberFormat.Invariant, ref buffer); CheckTrue(view.IsOver(buffer)); CheckEqual(0, view.StartIndex); CheckEqual(3, view.EndIndex); CheckEqual("abc", view.ToString()); } public void ScratchIsSizedForTheFormatsLongSeparators() { var wide = NumberFormatTests.Grouping("<->", 1, 1, "MINUS"); CheckEqual("MINUS9<->2<->2<->3<->3<->7<->2<->0<->3<->6<->8<->5<->4<->7<->7<->5<->8<->0<->8", CountText(long.MinValue, wide)); } public void NullFormat_Throws() { char[] buffer = null; var line = new TextPieces(0, 0); CheckThrow(typeof(ArgumentNullException)); line.ViewIn(null, ref buffer); } public void EmptyLine_IsAnEmptyViewOverAnArray() { char[] buffer = null; var line = new TextPieces(0, 0); StringView view = line.ViewIn(NumberFormat.Invariant, ref buffer); CheckEqual(0, view.Length); CheckNotNull(buffer); } public void NullScratch_IsReplacedByOneThatFits() { char[] buffer = null; var line = new TextPieces(0, 0); line.AppendFormatted("hello"); line.ViewIn(NumberFormat.Invariant, ref buffer); CheckTrue(buffer.Length >= 5); } public void ScratchThatFits_IsKept() { char[] buffer = new char[100]; char[] original = buffer; var line = new TextPieces(0, 0); line.AppendFormatted("hello"); line.ViewIn(NumberFormat.Invariant, ref buffer); CheckTrue(ReferenceEquals(original, buffer)); } public void ScratchExactlyTheSizeNeeded_IsKept() { char[] buffer = new char[5]; char[] original = buffer; var line = new TextPieces(0, 0); line.AppendFormatted("hello"); line.ViewIn(NumberFormat.Invariant, ref buffer); CheckTrue(ReferenceEquals(original, buffer)); } public void ScratchThatIsTooSmall_IsReplacedByAtLeastDouble() { char[] buffer = new char[10]; var line = new TextPieces(0, 0); line.AppendFormatted("0123456789ABC"); StringView view = line.ViewIn(NumberFormat.Invariant, ref buffer); CheckEqual(20, buffer.Length); CheckEqual("0123456789ABC", view.ToString()); } public void ScratchMuchTooSmall_IsReplacedByOneThatFits() { char[] buffer = new char[2]; var line = new TextPieces(0, 0); line.AppendFormatted(new string('x', 50)); StringView view = line.ViewIn(NumberFormat.Invariant, ref buffer); CheckEqual(50, buffer.Length); CheckEqual(new string('x', 50), view.ToString()); } public void ReadingAgain_GivesTheSameText() { char[] buffer = null; var line = new TextPieces(0, 0); line.AppendLiteral("a\n b "); line.AppendFormatted(Plain(-12L)); string first = line.ViewIn(NumberFormat.Invariant, ref buffer).ToString(); string second = line.ViewIn(NumberFormat.Invariant, ref buffer).ToString(); CheckEqual("a b -12", first); CheckEqual(first, second); } public void ReadingASecondLineIntoTheSameScratch_ReplacesTheFirstViewsText() { char[] buffer = null; var first = new TextPieces(0, 0); first.AppendFormatted("long first line"); var second = new TextPieces(0, 0); second.AppendFormatted("short"); StringView firstView = first.ViewIn(NumberFormat.Invariant, ref buffer); second.ViewIn(NumberFormat.Invariant, ref buffer); CheckEqual("shortfirst line", firstView.ToString()); } public void View_EqualsTheSameText() { char[] buffer = null; var line = new TextPieces(0, 0); line.AppendLiteral("Hello "); line.AppendFormatted(Plain(7L)); CheckTrue(line.ViewIn(NumberFormat.Invariant, ref buffer).Equals("Hello 7")); } } public class AppendFormattedStyledTests : TestFixture { public void StyledString_ReadsAsTheString() { string name = "Ada"; CheckEqual("Hello Ada!", Render($"Hello {Tint(Colors.Red, name)}!")); } public void StyledNumber_ReadsAsDigits() { int count = -42; CheckEqual("Got -42.", Render($"Got {Tint(Colors.Red, Plain(count))}.")); } public void NullStyledString_ReadsAsEmpty() { string name = null; CheckEqual("[]", Render($"[{Tint(Colors.Red, name)}]")); } public void DefaultStyled_ReadsAsEmpty() { var line = new TextPieces(0, 0); line.AppendFormatted(default(StyledText)); CheckEqual("", line.ToString()); CheckEqual(1, line.SegmentCount); } public void AddsOneSegment() { var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, "x")); CheckEqual(1, line.SegmentCount); } public void NestedStyles_AreMerged() { var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, Gamma(2, "x"))); CheckEqual(new TextStyle { TintColor = Colors.Red, CoverageGamma = 2 }, line.StyleOfSegment(0)); } public void EqualStyles_ShareOneSlot() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.StyleCapacity + 1; ++i) { line.AppendFormatted(Tint(Colors.Red, "x")); } CheckEqual(TextPieces.StyleCapacity + 1, line.SegmentCount); } public void AStyleThatSetsNothing_TakesNoSlot() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.StyleCapacity; ++i) { line.AppendFormatted(Tint(Color.FromArgb(i + 1, 0, 0), "x")); } line.AppendFormatted(new StyledText("x", default)); CheckNull(line.StyleOfSegment(TextPieces.StyleCapacity)); } public void ManyDifferentStyles_UpToTheCapacityFit() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.StyleCapacity; ++i) { line.AppendFormatted(Tint(Color.FromArgb(i + 1, 0, 0), "x")); } CheckEqual((Color?)Color.FromArgb(TextPieces.StyleCapacity, 0, 0), line.StyleOfSegment(TextPieces.StyleCapacity - 1).Value.TintColor); } public void OneMoreDifferentStyleThanTheCapacity_Throws() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.StyleCapacity; ++i) { line.AppendFormatted(Tint(Color.FromArgb(i + 1, 0, 0), "x")); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Tint(Color.FromArgb(200, 0, 0), "x")); } public void StyledStringsPastTheTextCapacity_Throw() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.TextCapacity; ++i) { line.AppendFormatted(Tint(Colors.Red, "x")); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Tint(Colors.Red, "x")); } public void StyledNumbersPastTheNumberCapacity_Throw() { var line = new TextPieces(0, 0); for (int i = 0; i < TextPieces.NumberCapacity; ++i) { line.AppendFormatted(Tint(Colors.Red, Plain(i))); } CheckThrow(typeof(InvalidOperationException)); line.AppendFormatted(Tint(Colors.Red, Plain(1))); } } public class StyleOfSegmentTests : TestFixture { public void ALiteral_HasNoStyle() { var line = new TextPieces(0, 0); line.AppendLiteral("abc"); CheckNull(line.StyleOfSegment(0)); } public void APlainStringAndNumberHole_HaveNoStyle() { var line = new TextPieces(0, 0); line.AppendFormatted("abc"); line.AppendFormatted(Plain(5L)); CheckNull(line.StyleOfSegment(0)); CheckNull(line.StyleOfSegment(1)); } public void AStyledHole_HasItsStyle() { var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, "abc")); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(0)); } public void EachSegment_KeepsItsOwnStyle() { var line = new TextPieces(0, 0); line.AppendLiteral("a"); line.AppendFormatted(Tint(Colors.Red, "b")); line.AppendFormatted("c"); line.AppendFormatted(Gamma(2, Plain(7))); line.AppendFormatted(Tint(Colors.Red, Plain(8))); CheckNull(line.StyleOfSegment(0)); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(1)); CheckNull(line.StyleOfSegment(2)); CheckEqual(new TextStyle { CoverageGamma = 2 }, line.StyleOfSegment(3)); CheckEqual(new TextStyle { TintColor = Colors.Red }, line.StyleOfSegment(4)); } public void SegmentsInLaterWordsOfTheStyleList_KeepTheirStyles() { var line = new TextPieces(0, 0); for (int i = 0; i < 30; ++i) { if (i % 3 == 0) { line.AppendFormatted(Tint(i % 2 == 0 ? Colors.Red : Colors.Blue, "x")); } else { line.AppendFormatted("x"); } } CheckNull(line.StyleOfSegment(16)); CheckEqual((Color?)Colors.Red, line.StyleOfSegment(18).Value.TintColor); CheckEqual((Color?)Colors.Blue, line.StyleOfSegment(27).Value.TintColor); CheckNull(line.StyleOfSegment(28)); } public void ANegativeSegment_Throws() { var line = new TextPieces(0, 0); line.AppendLiteral("abc"); CheckThrow(typeof(ArgumentOutOfRangeException)); line.StyleOfSegment(-1); } public void ASegmentPastTheLast_Throws() { var line = new TextPieces(0, 0); line.AppendLiteral("abc"); CheckThrow(typeof(ArgumentOutOfRangeException)); line.StyleOfSegment(1); } } public class ViewInWithSpansTests : TestFixture { private char[] scratch; private readonly DynamicArray spans = new DynamicArray(); [IgnoreTest] public int SpanCountOf(in TextPieces line) { line.ViewIn(NumberFormat.Invariant, ref scratch, spans); return spans.Count; } [UnitTestSharp.AllocationTest] public void ReadingAStyledLine_AllocatesNothingOnceTheBuffersHaveGrown() { int count = 7; int spanCount = SpanCountOf( $"You found {Tint(Colors.Gold, Count(count))} gold and {Gamma(2, "dust")}."); CheckNoAllocations(); CheckEqual(2, spanCount); } public void ALineWithNoStyledHoles_HasNoSpans() { CheckEqual("", DescribeSpans($"Hello {"Ada"}, you have {Plain(3)} things")); } public void AStyledHole_HasASpanOverItsCharacters() { int count = 5; CheckEqual("10-11:FFD700", DescribeSpans($"You found {Tint(Colors.Gold, Count(count))} gold")); } public void ASpanOfAStringCoversTheWholeString() { CheckEqual("4-7:FF0000", DescribeSpans($"Hi, {Tint(Colors.Red, "Ada")}!")); } public void ANegativeNumbersSpanIncludesTheSign() { long value = -12; CheckEqual("2-5:FF0000", DescribeSpans($"x {Tint(Colors.Red, Plain(value))}")); } public void SpansAreInOrder() { CheckEqual("0-1:FF0000 2-3:0000FF 4-5:FF0000", DescribeSpans($"{Tint(Colors.Red, "a")} {Tint(Colors.Blue, "b")} {Tint(Colors.Red, "c")}")); } public void NeighbouringHolesOfOneStyle_KeepSeparateSpans() { CheckEqual("0-1:FF0000 1-2:FF0000", DescribeSpans($"{Tint(Colors.Red, "a")}{Tint(Colors.Red, "b")}")); } public void AnEmptyStyledHole_HasNoSpan() { string empty = ""; CheckEqual("0-1:0000FF", DescribeSpans($"{Tint(Colors.Red, empty)}{Tint(Colors.Blue, "b")}")); } public void APlainHoleBetweenStyledOnes_HasNoSpan() { CheckEqual("0-1:FF0000 2-3:FF0000", DescribeSpans($"{Tint(Colors.Red, "a")}{"-"}{Tint(Colors.Red, "c")}")); } public void Positions_FollowTheFoldedLiterals() { var line = new TextPieces(0, 0); line.AppendLiteral("one\n two "); line.AppendFormatted(Tint(Colors.Red, "x")); CheckEqual("10-11:FF0000", DescribeSpans(line)); } public void Text_IsTheSameAsWithoutSpans() { var line = new TextPieces(0, 0); line.AppendLiteral("a\n b "); line.AppendFormatted(Tint(Colors.Red, Plain(-12))); char[] plain = null; StringView withSpans = line.ViewIn(NumberFormat.Invariant, ref scratch, spans); CheckEqual(line.ViewIn(NumberFormat.Invariant, ref plain).ToString(), withSpans.ToString()); } public void NullFormat_Throws() { char[] text = null; var line = new TextPieces(0, 0); CheckThrow(typeof(ArgumentNullException)); line.ViewIn(null, ref text, spans); } public void NullSpans_Throws() { char[] text = null; var line = new TextPieces(0, 0); CheckThrow(typeof(ArgumentNullException)); line.ViewIn(NumberFormat.Invariant, ref text, null); } public void EmptyArray_GrowsToHoldTheSpans() { var buffer = new DynamicArray(); char[] text = null; var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, "a")); line.AppendFormatted(Tint(Colors.Blue, "b")); line.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckEqual(2, buffer.Count); CheckTrue(buffer.Capacity >= 2); } public void ArrayThatFits_IsKept() { var buffer = new DynamicArray(10); StyleSpan[] original = buffer.Storage; char[] text = null; var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, "a")); line.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckTrue(ReferenceEquals(original, buffer.Storage)); } public void ArrayThatIsTooSmall_GrowsByAtLeastDouble() { var buffer = new DynamicArray(4); char[] text = null; var line = new TextPieces(0, 0); for (int i = 0; i < 5; ++i) { line.AppendFormatted(Tint(Colors.Red, "a")); } line.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckEqual(8, buffer.Capacity); CheckEqual(5, buffer.Count); } public void SpansFromAnEarlierRead_AreReplaced() { var buffer = new DynamicArray(); char[] text = null; var first = new TextPieces(0, 0); first.AppendFormatted(Tint(Colors.Red, "a")); first.AppendFormatted(Tint(Colors.Blue, "b")); first.AppendFormatted(Tint(Colors.Green, "c")); var second = new TextPieces(0, 0); second.AppendFormatted(Tint(Colors.Gold, "z")); first.ViewIn(NumberFormat.Invariant, ref text, buffer); second.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckEqual(1, buffer.Count); CheckEqual(new StyleSpan(0, 1, new TextStyle { TintColor = Colors.Gold }), buffer[0]); } public void SpansDroppedByAShorterRead_ReleaseTheirStyles() { var buffer = new DynamicArray(); char[] text = null; var first = new TextPieces(0, 0); first.AppendFormatted(Tint(Colors.Red, "a")); first.AppendFormatted(Tint(Colors.Blue, "b")); var second = new TextPieces(0, 0); second.AppendFormatted(Tint(Colors.Gold, "z")); first.ViewIn(NumberFormat.Invariant, ref text, buffer); second.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckEqual(default(StyleSpan), buffer.Storage[1]); } public void TheScratchGrowsAsInTheOtherOverload() { var buffer = new DynamicArray(); char[] text = new char[10]; var line = new TextPieces(0, 0); line.AppendFormatted(Tint(Colors.Red, "0123456789ABC")); StringView view = line.ViewIn(NumberFormat.Invariant, ref text, buffer); CheckEqual(20, text.Length); CheckEqual("0123456789ABC", view.ToString()); } public void ReadingAgain_GivesTheSameSpans() { var line = new TextPieces(0, 0); line.AppendLiteral("go "); line.AppendFormatted(Tint(Colors.Red, "x")); CheckEqual(DescribeSpans(line), DescribeSpans(line)); } } public class ToStringTests : TestFixture { public void StyledHoles_ReadAsPlainText() { CheckEqual("a b 7", Render($"a {Tint(Colors.Red, "b")} {Gamma(2, Plain(7))}")); } public void EmptyLine_IsTheEmptyString() { CheckEqual("", new TextPieces(0, 0).ToString()); } public void LiteralsStringsAndNumbers_AreJoinedInOrder() { string name = "Ada"; long balance = -250; CheckEqual("Ada owes 250? No: Ada has -250.", Render($"{name} owes 250? No: {name} has {Plain(balance)}.")); } public void HolesAndLiteralsInterleaved_KeepTheirOrder() { CheckEqual("1a2b3c", Render($"{Plain(1)}a{Plain(2)}b{Plain(3)}c")); } public void SegmentsPastTheFirstWordOfTheOrderList_KeepTheirKindAndSlot() { var line = new TextPieces(0, 0); for (int i = 0; i < 12; ++i) { line.AppendFormatted(Plain(i)); line.AppendFormatted("-"); } CheckEqual("0-1-2-3-4-5-6-7-8-9-10-11-", line.ToString()); } public void EachCallGivesAFreshString() { var line = new TextPieces(0, 0); line.AppendFormatted("a"); CheckFalse(ReferenceEquals(line.ToString(), line.ToString())); } } } }