File: DocumentReferencesTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Documents.Abstractions.Tests\Microsoft.Extensions.Documents.Abstractions.Tests.csproj (Microsoft.Extensions.Documents.Abstractions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Documents;
using Xunit;
 
namespace Microsoft.Extensions.Documents.Tests;
 
public class DocumentReferencesTests
{
    [Fact]
    public void ReducedApi_ExposesTypedSourcesWithoutOwnershipContract()
    {
        var assembly = typeof(ExtractionDocument).Assembly;
        foreach (string name in new[] { "DocumentAnnotation", "DocumentRegion", "DocumentConfidence", "DocumentPageReference", "DocumentEvidenceReference" })
        {
            Assert.Null(assembly.GetType("Microsoft.Extensions.Documents." + name));
        }
 
        foreach (Type type in new[] { typeof(ExtractionDocument), typeof(ExtractionDocumentElement), typeof(ExtractionDocumentTableCell) })
        {
            foreach (string name in new[] { "Annotations", "Evidence", "PageReferences", "HasMetadata", "Address", "LocalId", "Version", "IsDisposed" })
            {
                Assert.Null(type.GetProperty(name));
            }
        }
 
        Assert.Null(typeof(ExtractionDocument).GetProperty("Sources"));
        Assert.Null(typeof(ExtractionDocument).GetProperty("Artifacts"));
        foreach (Type type in new[] { typeof(ExtractionDocumentElement), typeof(ExtractionDocumentTableCell) })
        {
            var property = type.GetProperty("Sources")!;
            Assert.Equal(typeof(System.Collections.Generic.List<ExtractionDocumentSourceReference>), property.PropertyType);
            Assert.False(property.CanWrite);
        }
 
        Assert.False(typeof(ExtractionDocumentSourceReference).GetProperty("Page")!.CanWrite);
        Assert.False(typeof(ExtractionDocumentSourceReference).GetProperty("Polygon")!.CanWrite);
        Assert.False(typeof(ExtractionDocument).GetProperty("SourcePages")!.CanWrite);
        foreach (string name in new[] { "SetContent", "GetCoveringCell", "IsCovered" })
        {
            Assert.Null(typeof(ExtractionDocumentTableCellCollection).GetMethod(name));
        }
 
        foreach (string name in new[] { "GetCoveringCell", "IsCovered" })
        {
            var method = typeof(ExtractionDocumentTableCellCollection).GetMethod(name,
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
            Assert.True(method.IsAssembly);
        }
 
        Assert.Null(typeof(ExtractionDocumentTableCell).GetProperty("Kind"));
        Assert.Null(typeof(ExtractionDocumentTableCell).GetProperty("CellKind"));
        Assert.Equal(typeof(string), typeof(ExtractionDocumentTableCell).GetProperty("Role")!.PropertyType);
        Assert.False(typeof(ExtractionDocumentTableCell).GetProperty("Role")!.CanWrite);
        var indexer = typeof(ExtractionDocumentTableCellCollection).GetProperty("Item")!;
        Assert.Equal(typeof(ExtractionDocumentTableCell), indexer.PropertyType);
        Assert.True(indexer.CanRead);
        Assert.True(indexer.CanWrite);
        Assert.All(typeof(ExtractionDocumentTableCell).GetConstructors(), constructor =>
        {
            var parameters = constructor.GetParameters();
            var role = parameters[parameters.Length - 1];
            Assert.Equal("role", role.Name);
            Assert.Equal(typeof(string), role.ParameterType);
            Assert.True(role.IsOptional);
            Assert.Null(role.DefaultValue);
        });
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void SourceReference_NullOrEmptyPolygonIsPageOnly(bool empty)
    {
        ExtractionDocumentSourcePage page = new("unknown", 3);
        using DocumentAnnotationTestData.BorrowedNative native = new();
        ExtractionDocumentSourceReference reference = new(page, empty ? Array.Empty<ExtractionDocumentPoint>() : null, native);
        ExtractionDocumentSourceReference omitted = new(page);
        Assert.Same(page, reference.Page);
        Assert.Empty(reference.Polygon);
        Assert.Empty(omitted.Polygon);
        Assert.Null(omitted.GetRawRepresentation());
        Assert.Same(native, reference.GetRawRepresentation());
        Assert.Null(page.Dimensions);
        Assert.Null(page.CoordinateUnit);
        Assert.Null(page.CoordinateOrigin);
        Assert.Equal(0, native.DisposeCount);
    }
 
    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void SourceReference_RejectsShortPolygons(int count)
    {
        Assert.Throws<ArgumentException>("polygon", () => new ExtractionDocumentSourceReference(new("page", 1), new ExtractionDocumentPoint[count]));
    }
 
    [Fact]
    public void SourceReference_MinimumTriangleSnapshotsVerticesAndBorrowsNative()
    {
        ExtractionDocumentPoint[] points = [new(1, 2), new(3, 4), new(5, 6)];
        using DocumentAnnotationTestData.BorrowedNative native = new();
        ExtractionDocumentSourcePage page = new("triangle", 1);
        ExtractionDocumentSourceReference reference = new(page, points, native);
        points[2] = new(9, 10);
        Assert.Equal(new[] { new ExtractionDocumentPoint(1, 2), new ExtractionDocumentPoint(3, 4), new ExtractionDocumentPoint(5, 6) }, reference.Polygon);
        Assert.Same(page, reference.Page);
        Assert.Same(native, reference.GetRawRepresentation());
        Assert.Equal(0, native.DisposeCount);
    }
 
    [Fact]
    public void SourceReference_IteratorPolygonIsEnumeratedOnceAndSnapshotsMutableList()
    {
        List<ExtractionDocumentPoint> points = [new(1.25, 2.5), new(3.75, 4.5), new(5.25, 6.5)];
        ExtractionDocumentPoint[] expected = points.ToArray();
        int enumerations = 0;
        ExtractionDocumentSourcePage page = DocumentAnnotationTestData.PdfPage();
        using DocumentAnnotationTestData.BorrowedNative native = new();
 
        IEnumerable<ExtractionDocumentPoint> EnumerateOnce()
        {
            if (++enumerations != 1)
            {
                throw new InvalidOperationException("The polygon must only be enumerated once.");
            }
 
            foreach (ExtractionDocumentPoint point in points)
            {
                yield return point;
            }
        }
 
        ExtractionDocumentSourceReference reference = new(page, EnumerateOnce(), native);
        Assert.Equal(1, enumerations);
        points[0] = new(99, 100);
        points.RemoveAt(2);
        points.Add(new(101, 102));
        points.Add(new(103, 104));
 
        Assert.Equal(expected, reference.Polygon);
        Assert.Equal(expected, reference.Polygon.ToArray());
        Assert.Equal(1, enumerations);
        Assert.Same(page, reference.Page);
        Assert.Same(native, reference.GetRawRepresentation());
        Assert.Equal(0, native.DisposeCount);
        IList<ExtractionDocumentPoint> polygon = Assert.IsAssignableFrom<IList<ExtractionDocumentPoint>>(reference.Polygon);
        Assert.True(polygon.IsReadOnly);
        Assert.Throws<NotSupportedException>(() => polygon[0] = default);
        Assert.Throws<NotSupportedException>(() => polygon.Add(new(7, 8)));
        Assert.Throws<NotSupportedException>(() => polygon.Clear());
        Assert.Equal(expected, reference.Polygon);
    }
 
    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(2)]
    public void SourceReference_IteratorPolygonPreservesEmptyAndShortValidation(int count)
    {
        int enumerations = 0;
        IEnumerable<ExtractionDocumentPoint> EnumerateOnce()
        {
            if (++enumerations != 1)
            {
                throw new InvalidOperationException("The polygon must only be enumerated once.");
            }
 
            for (int i = 0; i < count; i++)
            {
                yield return new(i, i + 1);
            }
        }
 
        ExtractionDocumentSourcePage page = new("iterator", 1);
        if (count == 0)
        {
            ExtractionDocumentSourceReference reference = new(page, EnumerateOnce());
            Assert.Empty(reference.Polygon);
            Assert.Same(page, reference.Page);
            Assert.Null(reference.GetRawRepresentation());
        }
        else
        {
            Assert.Throws<ArgumentException>("polygon", () => new ExtractionDocumentSourceReference(page, EnumerateOnce()));
        }
 
        Assert.Equal(1, enumerations);
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    public void SourceIdentifiers_NullOrEmptyValue_Throws(string? value)
    {
        Assert.ThrowsAny<ArgumentException>(() => new ExtractionDocumentSourcePage(value!, 1));
    }
 
    [Fact]
    public void Geometry_PreservesOrderedFractionalVerticesAndFrameIdentity()
    {
        ExtractionDocument document = new("geometry");
        ExtractionDocumentPoint[] supplied = DocumentAnnotationTestData.Polygon();
        ExtractionDocumentSourcePage page = DocumentAnnotationTestData.PdfPage();
        ExtractionDocumentSourceReference region = new(page, supplied);
        supplied[0] = new ExtractionDocumentPoint(999, 999);
        ExtractionDocumentTextElement text = new(new TextContent("visible"));
        text.Sources.Add(region);
        ExtractionDocumentSection section = new(null);
        section.Elements.Add(text);
        document.Sections.Add(section);
        document.SourcePages.Add(page);
 
        Assert.Equal(DocumentAnnotationTestData.Polygon(), region.Polygon);
        Assert.Same(page, region.Page);
        Assert.Same(page, Assert.Single(document.SourcePages));
        Assert.Equal("urn:frame:pdf-A", page.Identifier);
        Assert.Equal(7, page.PageNumber);
        Assert.Equal(new ExtractionDocumentPageDimensions(200, 100), page.Dimensions);
        Assert.Equal(ExtractionDocumentCoordinateUnit.Point, page.CoordinateUnit);
        Assert.Equal(ExtractionDocumentCoordinateOrigin.BottomLeft, page.CoordinateOrigin);
        Assert.Throws<NotSupportedException>(() => ((System.Collections.Generic.IList<ExtractionDocumentPoint>)region.Polygon)[0] = default);
    }
 
    [Fact]
    public void Geometry_KnownPdfAndRasterFixturesProduceDistinctExpectedOverlayVertices()
    {
        ExtractionDocumentSourceReference pdf = new(DocumentAnnotationTestData.PdfPage(), DocumentAnnotationTestData.Polygon());
        ExtractionDocumentPoint[] expected =
        [
            new(20.5, 159), new(81.5, 163.5), new(89, 79.75),
            new(25.75, 82.5), new(17, 129.5)
        ];
        ExtractionDocumentSourcePage rasterPage = new("urn:frame:raster-A", 7, new ExtractionDocumentPageDimensions(400, 200),
            ExtractionDocumentCoordinateUnit.Pixel, ExtractionDocumentCoordinateOrigin.TopLeft);
        ExtractionDocumentSourceReference raster = new(rasterPage, expected);
 
        Assert.Equal(expected, ProjectKnownOverlay(pdf));
        Assert.Equal(expected, ProjectKnownOverlay(raster));
        Assert.Equal(DocumentAnnotationTestData.Polygon(), pdf.Polygon);
        Assert.Equal(new ExtractionDocumentPageDimensions(400, 200), raster.Page.Dimensions);
        Assert.NotEqual(pdf.Polygon[0], raster.Polygon[0]);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void Geometry_UnknownUnitOrOriginDoesNotGuessConversion(bool unknownUnit)
    {
        ExtractionDocumentSourcePage page = new("partial", 7, new ExtractionDocumentPageDimensions(200, 100),
            unknownUnit ? null : ExtractionDocumentCoordinateUnit.Point,
            unknownUnit ? ExtractionDocumentCoordinateOrigin.BottomLeft : null);
        ExtractionDocumentSourceReference region = new(page, DocumentAnnotationTestData.Polygon());
 
        Assert.Null(ProjectKnownOverlay(region));
        Assert.Equal(DocumentAnnotationTestData.Polygon(), region.Polygon);
        Assert.Equal(new ExtractionDocumentPageDimensions(200, 100), page.Dimensions);
        Assert.Equal(unknownUnit ? null : ExtractionDocumentCoordinateUnit.Point, page.CoordinateUnit);
        Assert.Equal(unknownUnit ? ExtractionDocumentCoordinateOrigin.BottomLeft : null, page.CoordinateOrigin);
    }
 
    [Fact]
    public void Geometry_UnknownDimensionsDoNotInferSizeFromKnownFrameOrPolygon()
    {
        ExtractionDocumentSourcePage page = new("no-size", 7, coordinateUnit: ExtractionDocumentCoordinateUnit.Point,
            coordinateOrigin: ExtractionDocumentCoordinateOrigin.BottomLeft);
        ExtractionDocumentSourceReference reference = new(page, DocumentAnnotationTestData.Polygon());
        Assert.Null(page.Dimensions);
        Assert.Equal(ExtractionDocumentCoordinateUnit.Point, page.CoordinateUnit);
        Assert.Equal(ExtractionDocumentCoordinateOrigin.BottomLeft, page.CoordinateOrigin);
        Assert.Equal(DocumentAnnotationTestData.Polygon(), reference.Polygon);
        Assert.Null(ProjectKnownOverlay(reference));
    }
 
    [Theory]
    [InlineData(double.NaN)]
    [InlineData(double.PositiveInfinity)]
    [InlineData(double.NegativeInfinity)]
    public void Geometry_RejectsNonFiniteValues(double value)
    {
        Assert.Throws<ArgumentOutOfRangeException>("x", () => new ExtractionDocumentPoint(value, 2));
        Assert.Throws<ArgumentOutOfRangeException>("y", () => new ExtractionDocumentPoint(2, value));
        Assert.Throws<ArgumentOutOfRangeException>("width", () => new ExtractionDocumentPageDimensions(value, 100));
        Assert.Throws<ArgumentOutOfRangeException>("height", () => new ExtractionDocumentPageDimensions(200, value));
        Assert.Throws<ArgumentOutOfRangeException>("width", () => new ExtractionDocumentPageDimensions(value, 0));
        Assert.Throws<ArgumentOutOfRangeException>("height", () => new ExtractionDocumentPageDimensions(0, value));
        ExtractionDocumentSourceReference valid = new(DocumentAnnotationTestData.PdfPage(), DocumentAnnotationTestData.Polygon());
        Assert.Equal(5, valid.Polygon.Count);
        Assert.Equal(60.125, valid.Polygon[2].Y);
    }
 
    [Fact]
    public void Dimensions_ExplicitEmptyEqualsDefaultWithoutTreatingPositiveDimensionsAsEmpty()
    {
        ExtractionDocumentPageDimensions empty = new(0, 0);
        ExtractionDocumentPageDimensions defaultDimensions = default;
        Assert.Equal(0, empty.Width);
        Assert.Equal(0, empty.Height);
        Assert.Equal(defaultDimensions, empty);
        Assert.True(empty.Equals(defaultDimensions));
        Assert.True(empty.Equals((object)defaultDimensions));
        Assert.True(empty == defaultDimensions);
        Assert.False(empty != defaultDimensions);
        Assert.Equal(defaultDimensions.GetHashCode(), empty.GetHashCode());
 
        ExtractionDocumentPageDimensions positive = new(double.Epsilon, double.Epsilon);
        Assert.Equal(double.Epsilon, positive.Width);
        Assert.Equal(double.Epsilon, positive.Height);
        Assert.NotEqual(empty, positive);
        Assert.False(empty == positive);
        Assert.True(empty != positive);
        ExtractionDocumentSourcePage page = new("tiny", 1, positive);
        Assert.Equal(positive, page.Dimensions);
    }
 
    [Theory]
    [InlineData(-1, 100, "width")]
    [InlineData(200, -1, "height")]
    [InlineData(-double.Epsilon, 100, "width")]
    [InlineData(200, -double.Epsilon, "height")]
    [InlineData(0, 100, "width")]
    [InlineData(200, 0, "height")]
    [InlineData(0, double.Epsilon, "width")]
    [InlineData(double.Epsilon, 0, "height")]
    [InlineData(-1, 0, "width")]
    [InlineData(0, -1, "width")]
    public void Dimensions_RejectsNegativeAndOneSidedZero(double width, double height, string parameter)
    {
        Assert.Throws<ArgumentOutOfRangeException>(parameter, () => new ExtractionDocumentPageDimensions(width, height));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void SourcePage_RejectsExplicitAndDefaultEmptyDimensionsButAllowsUnknown(bool explicitEmpty)
    {
        ExtractionDocumentPageDimensions dimensions = explicitEmpty ? new(0, 0) : default;
        Assert.Throws<ArgumentOutOfRangeException>("dimensions", () => new ExtractionDocumentSourcePage("empty", 1, dimensions));
 
        ExtractionDocumentSourcePage unknown = new("unknown", 1, dimensions: null);
        Assert.Null(unknown.Dimensions);
        Assert.Equal("unknown", unknown.Identifier);
        Assert.Equal(1, unknown.PageNumber);
        Assert.Null(unknown.CoordinateUnit);
        Assert.Null(unknown.CoordinateOrigin);
    }
 
    [Fact]
    public void SourceCarriers_RejectInvalidFramesAndReferences()
    {
        ExtractionDocumentSourcePage page = DocumentAnnotationTestData.PdfPage();
        Assert.Throws<ArgumentOutOfRangeException>("width", () => new ExtractionDocumentPageDimensions(0, 100));
        Assert.Throws<ArgumentOutOfRangeException>("height", () => new ExtractionDocumentPageDimensions(200, -1));
        Assert.Throws<ArgumentOutOfRangeException>("pageNumber", () => new ExtractionDocumentSourcePage("frame", 0));
        Assert.Throws<ArgumentOutOfRangeException>("dimensions", () => new ExtractionDocumentSourcePage("frame", 1, default(ExtractionDocumentPageDimensions)));
        Assert.Throws<ArgumentOutOfRangeException>("coordinateUnit", () => new ExtractionDocumentSourcePage("frame", 1, coordinateUnit: (ExtractionDocumentCoordinateUnit)99));
        Assert.Throws<ArgumentOutOfRangeException>("coordinateOrigin", () => new ExtractionDocumentSourcePage("frame", 1, coordinateOrigin: (ExtractionDocumentCoordinateOrigin)99));
        Assert.Throws<ArgumentNullException>("page", () => new ExtractionDocumentSourceReference(null!));
        Assert.Throws<ArgumentException>("polygon", () => new ExtractionDocumentSourceReference(page, new[] { new ExtractionDocumentPoint(1, 2), new ExtractionDocumentPoint(3, 4) }));
        ExtractionDocumentTextElement text = new(new TextContent("visible"));
        ExtractionDocumentTableCell cell = new();
        text.Sources.Add(null!);
        cell.Sources.Add(null!);
        Assert.Null(Assert.Single(text.Sources));
        Assert.Null(Assert.Single(cell.Sources));
    }
 
    // Consumer-owned oracle for these two known fixtures only; not a production conversion API.
    private static ExtractionDocumentPoint[]? ProjectKnownOverlay(ExtractionDocumentSourceReference region)
    {
        if (region.Page.Dimensions == new ExtractionDocumentPageDimensions(200, 100) &&
            region.Page.CoordinateUnit == ExtractionDocumentCoordinateUnit.Point &&
            region.Page.CoordinateOrigin == ExtractionDocumentCoordinateOrigin.BottomLeft)
        {
            ExtractionDocumentPoint[] result = new ExtractionDocumentPoint[region.Polygon.Count];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = new ExtractionDocumentPoint(2 * region.Polygon[i].X, 2 * (100 - region.Polygon[i].Y));
            }
 
            return result;
        }
 
        return region.Page.Dimensions == new ExtractionDocumentPageDimensions(400, 200) &&
            region.Page.CoordinateUnit == ExtractionDocumentCoordinateUnit.Pixel &&
            region.Page.CoordinateOrigin == ExtractionDocumentCoordinateOrigin.TopLeft
                ? System.Linq.Enumerable.ToArray(region.Polygon)
                : null;
    }
}