/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/Core/CodeAnalysisTest/Text/TextLineNewLineTests.cs
733 строки
27 KB
Cyrus Najmabadi
Improve TextLine and line table performance by packing existing data into unused bits (#83000)
16 апр 2026, 20:18
Не верифицирован
16 апр 2026, 20:18
0344ff9
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Text; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests.Text; /// <summary> /// Exhaustive tests for <see cref="TextLine"/> properties across all <see cref="SourceText"/> /// implementations and all supported line break sequences. /// </summary> public sealed class TextLineNewLineTests : TestBase { /// <summary> /// The four SourceText implementation strategies under test. /// </summary> public enum TextKind { String, Large, Sub, Composite } private static readonly TextKind[] s_allTextKinds = [TextKind.String, TextKind.Large, TextKind.Sub, TextKind.Composite]; private static readonly (string NewLine, int NewLineLength)[] s_newlines = [ ("\n", 1), ("\r", 1), ("\r\n", 2), ("\u0085", 1), ("\u2028", 1), ("\u2029", 1), ]; #region Theory data generators public static IEnumerable<object[]> AllNewlines { get { foreach (var (newLine, newLineLength) in s_newlines) yield return [newLine, newLineLength]; } } public static IEnumerable<object[]> AllTextKinds { get { foreach (var kind in s_allTextKinds) yield return [kind]; } } /// <summary>Cross-product of all newlines × all text kinds.</summary> public static IEnumerable<object[]> AllNewlinesAndKinds { get { foreach (var (newLine, newLineLength) in s_newlines) { foreach (var kind in s_allTextKinds) yield return [newLine, newLineLength, kind]; } } } /// <summary>Cross-product of all newline pairs × all text kinds.</summary> public static IEnumerable<object[]> AllNewlinePairsAndKinds { get { foreach (var (newLine1, newLineLength1) in s_newlines) { foreach (var (newLine2, newLineLength2) in s_newlines) { foreach (var kind in s_allTextKinds) yield return [newLine1, newLineLength1, newLine2, newLineLength2, kind]; } } } } /// <summary> /// Content patterns that exercise all interesting newline scenarios, used for cross-type /// consistency testing. /// </summary> public static IEnumerable<object[]> InterestingContents => [ [""], ["hello"], ["abc\ndef"], ["abc\rdef"], ["abc\r\ndef"], ["abc\u0085def"], ["abc\u2028def"], ["abc\u2029def"], ["\n"], ["\r"], ["\r\n"], ["\u0085"], ["\u2028"], ["\u2029"], ["\n\n\n"], ["\r\r\r"], ["\r\n\r\n\r\n"], ["abc\n\rdef"], ["a\r\r\nb"], ["a\r\n\rb"], ["a\nb\rc\r\nd\u0085e\u2028f\u2029g"], ["abc\r\n"], ["\r\nabc"], ["a\r\n\r\nb"], ["\n\r\n\r\n\r"], ]; /// <summary>Cross-product of interesting contents × all TextKind pairs.</summary> public static IEnumerable<object[]> InterestingContentsAndKindPairs { get { foreach (var contentData in InterestingContents) { foreach (var k1 in s_allTextKinds) { foreach (var k2 in s_allTextKinds) yield return [(string)contentData[0], k1, k2]; } } } } #endregion #region Helpers — SourceText creation /// <summary> /// Creates a <see cref="SourceText"/> with the given <paramref name="content"/> using the /// implementation strategy indicated by <paramref name="kind"/>. /// </summary> private static SourceText CreateText(string content, TextKind kind) => kind switch { TextKind.String => SourceText.From(content), TextKind.Large => CreateLargeText(content), TextKind.Sub => CreateSubText(content), TextKind.Composite => CreateCompositeFromContent(content), _ => throw new ArgumentOutOfRangeException(nameof(kind)), }; private static SourceText CreateLargeText(string content) { if (content.Length == 0) return SourceText.From(content); return LargeText.Decode(new StringReader(content), content.Length, Encoding.UTF8, SourceHashAlgorithm.Sha1); } /// <summary> /// Creates a SubText by embedding <paramref name="content"/> inside a larger text with /// non-newline padding, then extracting the inner portion. This proves that line properties /// are not influenced by the surrounding text. /// </summary> private static SourceText CreateSubText(string content) { const string prefix = "PADDING_PREFIX_"; const string suffix = "_SUFFIX_PADDING"; var full = SourceText.From(prefix + content + suffix); return new SubText(full, new TextSpan(prefix.Length, content.Length)); } /// <summary> /// Creates a CompositeText by splitting <paramref name="content"/> at the midpoint. /// For very short content where a meaningful split isn't possible, falls back to StringText. /// </summary> private static SourceText CreateCompositeFromContent(string content) { if (content.Length < 2) return SourceText.From(content); var mid = content.Length / 2; return CreateCompositeText(content[..mid], content[mid..]); } private static SourceText CreateLargeTextWithChunks(params string[] chunks) { var builder = ImmutableArray.CreateBuilder<char[]>(chunks.Length); foreach (var chunk in chunks) builder.Add(chunk.ToCharArray()); return new LargeText(builder.MoveToImmutable(), Encoding.UTF8, SourceHashAlgorithm.Sha1); } private static SourceText CreateCompositeText(params string[] segments) { var builder = ArrayBuilder<SourceText>.GetInstance(); foreach (var segment in segments) builder.Add(SourceText.From(segment)); var reference = SourceText.From(string.Concat(segments)); var result = CompositeText.ToSourceText(builder, reference, adjustSegments: false); builder.Free(); return result; } #endregion #region Helpers — assertion private static void CheckLine( SourceText text, int lineNumber, int start, int length, int newLineLength, string lineText) { var line = text.Lines[lineNumber]; Assert.Equal(start, line.Start); Assert.Equal(start + length, line.End); Assert.Equal(start + length + newLineLength, line.EndIncludingLineBreak); Assert.Equal(length, line.Span.Length); Assert.Equal(length + newLineLength, line.SpanIncludingLineBreak.Length); Assert.Equal(lineText, line.ToString()); } /// <summary> /// Asserts that every line property of <paramref name="actual"/> matches the corresponding /// line in <paramref name="reference"/>, and that <c>Lines.IndexOf</c> agrees for every /// position in the text. /// </summary> private static void AssertLinesMatch(SourceText reference, SourceText actual) { Assert.Equal(reference.Length, actual.Length); Assert.Equal(reference.Lines.Count, actual.Lines.Count); for (var i = 0; i < reference.Lines.Count; i++) { var referenceLine = reference.Lines[i]; var actualLine = actual.Lines[i]; Assert.Equal(referenceLine.Start, actualLine.Start); Assert.Equal(referenceLine.End, actualLine.End); Assert.Equal(referenceLine.EndIncludingLineBreak, actualLine.EndIncludingLineBreak); Assert.Equal(referenceLine.Span, actualLine.Span); Assert.Equal(referenceLine.SpanIncludingLineBreak, actualLine.SpanIncludingLineBreak); Assert.Equal(referenceLine.ToString(), actualLine.ToString()); } for (var pos = 0; pos < reference.Length; pos++) Assert.Equal(reference.Lines.IndexOf(pos), actual.Lines.IndexOf(pos)); } #endregion // ========================================================================= // Part 1: Ground truth — verify exact Start/End/EndIncludingLineBreak // values. Every test is parameterized over all TextKind variants // so that StringText, LargeText, SubText, and CompositeText are // all validated against the same expected values. // ========================================================================= [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void NewlineInMiddle(string newLine, int newLineLength, TextKind kind) { var text = CreateText("abc" + newLine + "def", kind); Assert.Equal(2, text.Lines.Count); CheckLine(text, 0, start: 0, length: 3, newLineLength: newLineLength, lineText: "abc"); CheckLine(text, 1, start: 3 + newLineLength, length: 3, newLineLength: 0, lineText: "def"); } [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void NewlineAtEnd(string newLine, int newLineLength, TextKind kind) { var text = CreateText("abc" + newLine, kind); Assert.Equal(2, text.Lines.Count); CheckLine(text, 0, start: 0, length: 3, newLineLength: newLineLength, lineText: "abc"); CheckLine(text, 1, start: 3 + newLineLength, length: 0, newLineLength: 0, lineText: ""); } [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void NewlineAtStart(string newLine, int newLineLength, TextKind kind) { var text = CreateText(newLine + "abc", kind); Assert.Equal(2, text.Lines.Count); CheckLine(text, 0, start: 0, length: 0, newLineLength: newLineLength, lineText: ""); CheckLine(text, 1, start: newLineLength, length: 3, newLineLength: 0, lineText: "abc"); } [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void OnlyNewline(string newLine, int newLineLength, TextKind kind) { var text = CreateText(newLine, kind); Assert.Equal(2, text.Lines.Count); CheckLine(text, 0, start: 0, length: 0, newLineLength: newLineLength, lineText: ""); CheckLine(text, 1, start: newLineLength, length: 0, newLineLength: 0, lineText: ""); } [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void ConsecutiveSameNewlines(string newLine, int newLineLength, TextKind kind) { var text = CreateText(newLine + newLine, kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 0, newLineLength: newLineLength, lineText: ""); CheckLine(text, 1, start: newLineLength, length: 0, newLineLength: newLineLength, lineText: ""); CheckLine(text, 2, start: 2 * newLineLength, length: 0, newLineLength: 0, lineText: ""); } [Theory, MemberData(nameof(AllNewlinesAndKinds))] public void ThreeConsecutiveSameNewlines(string newLine, int newLineLength, TextKind kind) { var text = CreateText(newLine + newLine + newLine, kind); Assert.Equal(4, text.Lines.Count); for (var i = 0; i < 3; i++) CheckLine(text, i, start: i * newLineLength, length: 0, newLineLength: newLineLength, lineText: ""); CheckLine(text, 3, start: 3 * newLineLength, length: 0, newLineLength: 0, lineText: ""); } [Theory, MemberData(nameof(AllNewlinePairsAndKinds))] public void TwoDifferentNewlines(string newLine1, int newLine1Length, string newLine2, int newLine2Length, TextKind kind) { var text = CreateText("a" + newLine1 + "b" + newLine2 + "c", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 1, newLineLength: newLine1Length, lineText: "a"); CheckLine(text, 1, start: 1 + newLine1Length, length: 1, newLineLength: newLine2Length, lineText: "b"); CheckLine(text, 2, start: 1 + newLine1Length + 1 + newLine2Length, length: 1, newLineLength: 0, lineText: "c"); } [Theory, MemberData(nameof(AllTextKinds))] public void Empty(TextKind kind) { var text = CreateText("", kind); Assert.Equal(1, text.Lines.Count); CheckLine(text, 0, start: 0, length: 0, newLineLength: 0, lineText: ""); } [Theory, MemberData(nameof(AllTextKinds))] public void NoNewlines(TextKind kind) { var text = CreateText("hello world", kind); Assert.Equal(1, text.Lines.Count); CheckLine(text, 0, start: 0, length: 11, newLineLength: 0, lineText: "hello world"); } [Theory, MemberData(nameof(AllTextKinds))] public void CRNotFollowedByLF(TextKind kind) { var text = CreateText("abc\rdef\nghi", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 3, newLineLength: 1, lineText: "abc"); CheckLine(text, 1, start: 4, length: 3, newLineLength: 1, lineText: "def"); CheckLine(text, 2, start: 8, length: 3, newLineLength: 0, lineText: "ghi"); } [Theory, MemberData(nameof(AllTextKinds))] public void LFThenCR_AreSeparateBreaks(TextKind kind) { var text = CreateText("abc\n\rdef", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 3, newLineLength: 1, lineText: "abc"); CheckLine(text, 1, start: 4, length: 0, newLineLength: 1, lineText: ""); CheckLine(text, 2, start: 5, length: 3, newLineLength: 0, lineText: "def"); } [Theory, MemberData(nameof(AllTextKinds))] public void AllSixNewlineTypes(TextKind kind) { var text = CreateText("a\nb\rc\r\nd\u0085e\u2028f\u2029g", kind); Assert.Equal(7, text.Lines.Count); CheckLine(text, 0, start: 0, length: 1, newLineLength: 1, lineText: "a"); CheckLine(text, 1, start: 2, length: 1, newLineLength: 1, lineText: "b"); CheckLine(text, 2, start: 4, length: 1, newLineLength: 2, lineText: "c"); CheckLine(text, 3, start: 7, length: 1, newLineLength: 1, lineText: "d"); CheckLine(text, 4, start: 9, length: 1, newLineLength: 1, lineText: "e"); CheckLine(text, 5, start: 11, length: 1, newLineLength: 1, lineText: "f"); CheckLine(text, 6, start: 13, length: 1, newLineLength: 0, lineText: "g"); } [Theory, MemberData(nameof(AllTextKinds))] public void ConsecutiveCRLF(TextKind kind) { var text = CreateText("a\r\n\r\nb", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 1, newLineLength: 2, lineText: "a"); CheckLine(text, 1, start: 3, length: 0, newLineLength: 2, lineText: ""); CheckLine(text, 2, start: 5, length: 1, newLineLength: 0, lineText: "b"); } [Theory, MemberData(nameof(AllTextKinds))] public void CRThenCRLF(TextKind kind) { var text = CreateText("a\r\r\nb", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 1, newLineLength: 1, lineText: "a"); CheckLine(text, 1, start: 2, length: 0, newLineLength: 2, lineText: ""); CheckLine(text, 2, start: 4, length: 1, newLineLength: 0, lineText: "b"); } [Theory, MemberData(nameof(AllTextKinds))] public void CRLFThenCR(TextKind kind) { var text = CreateText("a\r\n\rb", kind); Assert.Equal(3, text.Lines.Count); CheckLine(text, 0, start: 0, length: 1, newLineLength: 2, lineText: "a"); CheckLine(text, 1, start: 3, length: 0, newLineLength: 1, lineText: ""); CheckLine(text, 2, start: 4, length: 1, newLineLength: 0, lineText: "b"); } // ========================================================================= // Part 2: Cross-type consistency — verify that every pair of SourceText // implementations produces identical line information for the same // content. This catches any disagreement between implementations // even if the ground truth expectations above were wrong. // ========================================================================= [Theory, MemberData(nameof(InterestingContentsAndKindPairs))] public void CrossType_SameContent_SameLines(string content, TextKind kind1, TextKind kind2) { var text1 = CreateText(content, kind1); var text2 = CreateText(content, kind2); AssertLinesMatch(text1, text2); } // ========================================================================= // Part 3: Type-specific edge cases that exercise implementation details // unique to each SourceText subclass. // ========================================================================= // --- LargeText: chunk boundary tests --- [Theory] [InlineData("\n")] [InlineData("\r")] [InlineData("\r\n")] [InlineData("\u0085")] [InlineData("\u2028")] [InlineData("\u2029")] public void LargeText_ChunkBoundaryBeforeNewline(string newLine) { AssertLinesMatch( SourceText.From("abc" + newLine + "def"), CreateLargeTextWithChunks("abc", newLine + "def")); } [Theory] [InlineData("\n")] [InlineData("\r")] [InlineData("\r\n")] [InlineData("\u0085")] [InlineData("\u2028")] [InlineData("\u2029")] public void LargeText_ChunkBoundaryAfterNewline(string newLine) { AssertLinesMatch( SourceText.From("abc" + newLine + "def"), CreateLargeTextWithChunks("abc" + newLine, "def")); } [Fact] public void LargeText_ChunkBoundarySplitsCRLF() { var text = CreateLargeTextWithChunks("abc\r", "\ndef"); AssertLinesMatch(SourceText.From("abc\r\ndef"), text); Assert.Equal(2, text.Lines.Count); CheckLine(text, 0, start: 0, length: 3, newLineLength: 2, lineText: "abc"); CheckLine(text, 1, start: 5, length: 3, newLineLength: 0, lineText: "def"); } [Fact] public void LargeText_ChunkBoundarySplitsCRLF_OnlyNewlines() { AssertLinesMatch(SourceText.From("\r\n"), CreateLargeTextWithChunks("\r", "\n")); } [Fact] public void LargeText_ChunkBoundaryCRNotFollowedByLF() { AssertLinesMatch( SourceText.From("abc\rdef"), CreateLargeTextWithChunks("abc\r", "def")); } [Fact] public void LargeText_ChunkBoundary_MultipleCRLFSplits() { AssertLinesMatch( SourceText.From("ab\r\ncd\r\nef"), CreateLargeTextWithChunks("ab\r", "\ncd\r", "\nef")); } [Fact] public void LargeText_ChunkBoundary_CRAtEndThenCRLFSplit() { AssertLinesMatch( SourceText.From("a\rb\r\nc"), CreateLargeTextWithChunks("a\rb\r", "\nc")); } [Theory] [InlineData("abc\ndef\rghi\r\njkl")] [InlineData("\r\n\r\n\r\n")] [InlineData("\r\r\r")] [InlineData("\n\n\n")] [InlineData("a\u0085b\u2028c\u2029d")] [InlineData("a\r\nb\r\nc\r\n")] [InlineData("\ra\n\rb\n\rc\n")] public void LargeText_EachCharIsOwnChunk(string content) { var chunks = new string[content.Length]; for (var i = 0; i < content.Length; i++) chunks[i] = content[i].ToString(); AssertLinesMatch(SourceText.From(content), CreateLargeTextWithChunks(chunks)); } // --- SubText: CRLF splitting at SubText boundaries --- [Fact] public void SubText_SplitsCRLF_EndAtCR() { var fullText = SourceText.From("abc\r\ndef"); var subText = new SubText(fullText, new TextSpan(0, 4)); AssertLinesMatch(SourceText.From("abc\r"), subText); Assert.Equal(2, subText.Lines.Count); CheckLine(subText, 0, start: 0, length: 3, newLineLength: 1, lineText: "abc"); } [Fact] public void SubText_SplitsCRLF_StartAtLF() { var fullText = SourceText.From("abc\r\ndef"); AssertLinesMatch( SourceText.From("\ndef"), new SubText(fullText, new TextSpan(4, 4))); } [Fact] public void SubText_SplitsCRLF_JustCR() { var fullText = SourceText.From("abc\r\ndef"); AssertLinesMatch( SourceText.From("\r"), new SubText(fullText, new TextSpan(3, 1))); } [Fact] public void SubText_SplitsCRLF_JustLF() { var fullText = SourceText.From("abc\r\ndef"); AssertLinesMatch( SourceText.From("\n"), new SubText(fullText, new TextSpan(4, 1))); } [Fact] public void SubText_SplitsCRLF_AtStart() { var fullText = SourceText.From("\r\nabc"); AssertLinesMatch(SourceText.From("\r"), new SubText(fullText, new TextSpan(0, 1))); AssertLinesMatch(SourceText.From("\nabc"), new SubText(fullText, new TextSpan(1, 4))); } [Fact] public void SubText_SplitsCRLF_AtEnd() { var fullText = SourceText.From("abc\r\n"); AssertLinesMatch(SourceText.From("abc\r"), new SubText(fullText, new TextSpan(0, 4))); AssertLinesMatch(SourceText.From("\n"), new SubText(fullText, new TextSpan(4, 1))); } [Fact] public void SubText_SplitsCRLF_MultiplePairs() { var fullText = SourceText.From("a\r\nb\r\nc"); AssertLinesMatch(SourceText.From("a\r"), new SubText(fullText, new TextSpan(0, 2))); AssertLinesMatch(SourceText.From("\nb\r\nc"), new SubText(fullText, new TextSpan(2, 5))); } [Theory] [InlineData("abc\ndef\rghi\r\njkl")] [InlineData("\r\n\r\n\r\n")] [InlineData("\r\r\r")] [InlineData("\n\n\n")] [InlineData("a\u0085b\u2028c\u2029d")] [InlineData("\r\n")] [InlineData("x")] [InlineData("ab\r\ncd")] [InlineData("\n\r")] [InlineData("\r\n\r")] [InlineData("\n\r\n")] public void SubText_AllSubstrings_MatchStringText(string content) { var fullText = SourceText.From(content); for (var start = 0; start < content.Length; start++) { for (var end = start + 1; end <= content.Length; end++) { var reference = SourceText.From(content[start..end]); var subText = new SubText(fullText, new TextSpan(start, end - start)); AssertLinesMatch(reference, subText); } } } // --- CompositeText: segment boundary tests --- [Fact] public void CompositeText_CRLFSpansBoundary() { var composite = CreateCompositeText("abc\r", "\ndef"); AssertLinesMatch(SourceText.From("abc\r\ndef"), composite); } [Fact] public void CompositeText_CRLFSpansBoundary_MultipleTimes() { AssertLinesMatch( SourceText.From("ab\r\ncd\r\nef"), CreateCompositeText("ab\r", "\ncd\r", "\nef")); } [Fact] public void CompositeText_CRAtEndOfSegment_NotFollowedByLF() { AssertLinesMatch( SourceText.From("abc\rdef"), CreateCompositeText("abc\r", "def")); } [Theory, MemberData(nameof(InterestingContents))] public void CompositeText_AllSplitPoints_MatchStringText(string content) { var reference = SourceText.From(content); for (var split = 1; split < content.Length; split++) { var composite = CreateCompositeText(content[..split], content[split..]); AssertLinesMatch(reference, composite); } } [Theory, MemberData(nameof(InterestingContents))] public void CompositeText_ThreeWaySplit_AllCombinations(string content) { var reference = SourceText.From(content); for (var split1 = 1; split1 < content.Length - 1; split1++) { for (var split2 = split1 + 1; split2 < content.Length; split2++) { var composite = CreateCompositeText( content[..split1], content[split1..split2], content[split2..]); AssertLinesMatch(reference, composite); } } } // ========================================================================= // Part 4: TextLine Equals / GetHashCode — verify that the packed _data // representation correctly distinguishes lines and that equal lines // produce identical hash codes. // ========================================================================= [Fact] public void TextLine_SameLineIsEqual() { var text = SourceText.From("abc\ndef"); var line0a = text.Lines[0]; var line0b = text.Lines[0]; Assert.Equal(line0a, line0b); Assert.True(line0a == line0b); Assert.False(line0a != line0b); Assert.Equal(line0a.GetHashCode(), line0b.GetHashCode()); } [Fact] public void TextLine_DifferentLinesAreNotEqual() { var text = SourceText.From("abc\ndef"); var line0 = text.Lines[0]; var line1 = text.Lines[1]; Assert.NotEqual(line0, line1); Assert.False(line0 == line1); Assert.True(line0 != line1); } [Fact] public void TextLine_SamePositionDifferentBreakLengths_AreNotEqual() { // "abc\n" has line 0 with break length 1 // "abc\r\n" has line 0 with break length 2 // Both lines start at 0 and have text "abc", but the break differs. var textLF = SourceText.From("abc\ndef"); var textCRLF = SourceText.From("abc\r\ndef"); var lineLF = textLF.Lines[0]; var lineCRLF = textCRLF.Lines[0]; Assert.Equal(lineLF.Start, lineCRLF.Start); Assert.Equal(lineLF.End, lineCRLF.End); Assert.NotEqual(lineLF.EndIncludingLineBreak, lineCRLF.EndIncludingLineBreak); Assert.NotEqual(lineLF, lineCRLF); } [Fact] public void TextLine_DifferentTextInstances_AreNotEqual() { var text1 = SourceText.From("abc"); var text2 = SourceText.From("abc"); var line1 = text1.Lines[0]; var line2 = text2.Lines[0]; Assert.Equal(line1.Start, line2.Start); Assert.Equal(line1.End, line2.End); Assert.NotEqual(line1, line2); } [Fact] public void TextLine_EqualsObject() { var text = SourceText.From("abc\ndef"); var line = text.Lines[0]; Assert.True(line.Equals((object)text.Lines[0])); Assert.False(line.Equals((object)text.Lines[1])); Assert.False(line.Equals("not a TextLine")); Assert.False(line.Equals(null)); } [Theory, MemberData(nameof(AllTextKinds))] public void TextLine_EqualLines_HaveEqualHashCodes(TextKind kind) { var text = CreateText("abc\ndef\r\nghi", kind); for (var i = 0; i < text.Lines.Count; i++) { var lineA = text.Lines[i]; var lineB = text.Lines[i]; Assert.Equal(lineA, lineB); Assert.Equal(lineA.GetHashCode(), lineB.GetHashCode()); } } }