Skip to content

Commit

Permalink
[FormRecognizer] Add test coverage to FormLayoutClient (Azure#10921)
Browse files Browse the repository at this point in the history
  • Loading branch information
kinelski authored Mar 31, 2020
1 parent 33758e3 commit 66c6b39
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public FormLayoutClient(Uri endpoint, FormRecognizerApiKeyCredential credential,
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>A Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt; to wait on this long-running operation. Its Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt;.Value upon successful
/// completion will contain layout elements extracted from the form.</returns>
[ForwardsClientCalls]
public virtual Operation<IReadOnlyList<ExtractedLayoutPage>> StartExtractLayouts(Stream stream, ContentType contentType, CancellationToken cancellationToken = default)
{
// TODO: automate content-type detection
Expand All @@ -75,6 +76,7 @@ public virtual Operation<IReadOnlyList<ExtractedLayoutPage>> StartExtractLayouts
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>A Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt; to wait on this long-running operation. Its Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt;.Value upon successful
/// completion will contain layout elements extracted from the form.</returns>
[ForwardsClientCalls]
public virtual async Task<Operation<IReadOnlyList<ExtractedLayoutPage>>> StartExtractLayoutsAsync(Stream stream, ContentType contentType, CancellationToken cancellationToken = default)
{
// TODO: automate content-type detection
Expand All @@ -90,6 +92,7 @@ public virtual async Task<Operation<IReadOnlyList<ExtractedLayoutPage>>> StartEx
/// <param name="cancellationToken"></param>
/// <returns>A Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt; to wait on this long-running operation. Its Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt;.Value upon successful
/// completion will contain layout elements extracted from the form.</returns>
[ForwardsClientCalls]
public virtual Operation<IReadOnlyList<ExtractedLayoutPage>> StartExtractLayouts(Uri uri, CancellationToken cancellationToken = default)
{
SourcePath_internal sourcePath = new SourcePath_internal() { Source = uri.ToString() };
Expand All @@ -104,6 +107,7 @@ public virtual Operation<IReadOnlyList<ExtractedLayoutPage>> StartExtractLayouts
/// <param name="cancellationToken"></param>
/// <returns>A Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt; to wait on this long-running operation. Its Operation&lt;IReadOnlyList&lt;ExtractedLayoutPage&gt;&gt;.Value upon successful
/// completion will contain layout elements extracted from the form.</returns>
[ForwardsClientCalls]
public virtual async Task<Operation<IReadOnlyList<ExtractedLayoutPage>>> StartExtractLayoutsAsync(Uri uri, CancellationToken cancellationToken = default)
{
SourcePath_internal sourcePath = new SourcePath_internal() { Source = uri.ToString() };
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Azure.AI.FormRecognizer.Models;
using Azure.Core.Testing;
using NUnit.Framework;

namespace Azure.AI.FormRecognizer.Tests
{
/// <summary>
/// The suite of tests for the <see cref="FormLayoutClient"/> class.
/// </summary>
/// <remarks>
/// These tests have a dependency on live Azure services and may incur costs for the associated
/// Azure subscription.
/// </remarks>
[LiveOnly]
public class FormLayoutClientLiveTests : ClientTestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="FormLayoutClientLiveTests"/> class.
/// </summary>
/// <param name="isAsync">A flag used by the Azure Core Test Framework to differentiate between tests for asynchronous and synchronous methods.</param>
public FormLayoutClientLiveTests(bool isAsync) : base(isAsync)
{
}

/// <summary>
/// Verifies that the <see cref="FormLayoutClient" /> is able to connect to the Form
/// Recognizer cognitive service and perform operations.
/// </summary>
[Test]
[TestCase(true)]
[TestCase(false, Ignore = "The Invoice_1.pdf hasn't been uploaded to GitHub yet.")]
public async Task StartExtractLayoutsPopulatesExtractedLayoutPage(bool useStream)
{
var client = CreateInstrumentedClient();
Operation<IReadOnlyList<ExtractedLayoutPage>> operation;

if (useStream)
{
using var stream = new FileStream(TestEnvironment.RetrieveInvoicePath(1), FileMode.Open);
operation = await client.StartExtractLayoutsAsync(stream, ContentType.Jpeg);
}
else
{
var uri = new Uri(TestEnvironment.RetrieveInvoiceUri(1));
operation = await client.StartExtractLayoutsAsync(uri);
}

await operation.WaitForCompletionAsync();

Assert.IsTrue(operation.HasValue);

var layoutPage = operation.Value.Single();

// The expected values are based on the values returned by the service, and not the actual
// values present in the form. We are not testing the service here, but the SDK.

Assert.AreEqual(1, layoutPage.PageNumber);

var rawPage = layoutPage.RawExtractedPage;

Assert.AreEqual(1, rawPage.Page);
Assert.AreEqual(LengthUnit.Inch, rawPage.Unit);
Assert.AreEqual(8.5, rawPage.Width);
Assert.AreEqual(11, rawPage.Height);
Assert.AreEqual(0, rawPage.Angle);
Assert.AreEqual(18, rawPage.Lines.Count);

var lines = rawPage.Lines.ToList();

for (var lineIndex = 0; lineIndex < lines.Count; lineIndex++)
{
var line = lines[lineIndex];

Assert.NotNull(line.Text, $"Text should not be null in line {lineIndex}.");
Assert.Greater(line.Words.Count, 0, $"There should be at least one word in line {lineIndex}.");
Assert.AreEqual(4, line.BoundingBox.Points.Count(), $"There should be exactly 4 points in the bounding box in line {lineIndex}.");
}

var table = layoutPage.Tables.Single();

Assert.AreEqual(2, table.RowCount);
Assert.AreEqual(6, table.ColumnCount);

var cells = table.Cells.ToList();

Assert.AreEqual(10, cells.Count);

var expectedText = new string[2, 6]
{
{ "Invoice Number", "Invoice Date", "Invoice Due Date", "Charges", "", "VAT ID" },
{ "34278587", "6/18/2017", "6/24/2017", "$56,651.49", "", "PT" }
};

foreach (var cell in cells)
{
Assert.GreaterOrEqual(cell.RowIndex, 0, $"Cell with text {cell.Text} should have row index greater than or equal to zero.");
Assert.Less(cell.RowIndex, table.RowCount, $"Cell with text {cell.Text} should have row index less than {table.RowCount}.");
Assert.GreaterOrEqual(cell.ColumnIndex, 0, $"Cell with text {cell.Text} should have column index greater than or equal to zero.");
Assert.Less(cell.ColumnIndex, table.ColumnCount, $"Cell with text {cell.Text} should have column index less than {table.ColumnCount}.");

// There's a single cell in the table (row = 1, column = 3) that has a column span of 2.

var expectedColumnSpan = (cell.RowIndex == 1 && cell.ColumnIndex == 3) ? 2 : 1;

Assert.AreEqual(1, cell.RowSpan, $"Cell with text {cell.Text} should have a row span of 1.");
Assert.AreEqual(expectedColumnSpan, cell.ColumnSpan, $"Cell with text {cell.Text} should have a column span of {expectedColumnSpan}.");

Assert.AreEqual(expectedText[cell.RowIndex, cell.ColumnIndex], cell.Text);

Assert.IsFalse(cell.IsFooter, $"Cell with text {cell.Text} should not have been classified as footer.");
Assert.IsFalse(cell.IsHeader, $"Cell with text {cell.Text} should not have been classified as header.");

Assert.GreaterOrEqual(cell.Confidence, 0, $"Cell with text {cell.Text} should have confidence greater than or equal to zero.");
Assert.LessOrEqual(cell.RowIndex, 1, $"Cell with text {cell.Text} should have confidence less than or equal to one.");

Assert.Greater(cell.RawExtractedItems.Count, 0, $"Cell with text {cell.Text} should have at least one extracted item.");
}
}

/// <summary>
/// Creates a <see cref="FormLayoutClient" /> with the endpoint and API key provided via environment
/// variables and instruments it to make use of the Azure Core Test Framework functionalities.
/// </summary>
/// <returns>The instrumented <see cref="FormLayoutClient" />.</returns>
private FormLayoutClient CreateInstrumentedClient()
{
var endpointEnvironmentVariable = Environment.GetEnvironmentVariable(TestEnvironment.EndpointEnvironmentVariableName);
var keyEnvironmentVariable = Environment.GetEnvironmentVariable(TestEnvironment.ApiKeyEnvironmentVariableName);

Assert.NotNull(endpointEnvironmentVariable);
Assert.NotNull(keyEnvironmentVariable);

var endpoint = new Uri(endpointEnvironmentVariable);
var credential = new FormRecognizerApiKeyCredential(keyEnvironmentVariable);
var client = new FormLayoutClient(endpoint, credential);

return InstrumentClient(client);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.FormRecognizer.Models;
using Azure.Core.Testing;
using NUnit.Framework;

namespace Azure.AI.FormRecognizer.Tests
{
/// <summary>
/// The suite of tests for the <see cref="FormLayoutClient"/> class.
/// </summary>
public class FormLayoutClientTests : ClientTestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="FormLayoutClientTests"/> class.
/// </summary>
/// <param name="isAsync">A flag used by the Azure Core Test Framework to differentiate between tests for asynchronous and synchronous methods.</param>
public FormLayoutClientTests(bool isAsync) : base(isAsync)
{
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient"/> constructors.
/// </summary>
[Test]
[Ignore("Argument validation not implemented yet.")]
public void ConstructorRequiresTheEndpoint()
{
var credential = new FormRecognizerApiKeyCredential("key");

Assert.Throws<ArgumentNullException>(() => new FormLayoutClient(null, credential));
Assert.Throws<ArgumentNullException>(() => new FormLayoutClient(null, credential, new FormRecognizerClientOptions()));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient"/> constructors.
/// </summary>
[Test]
[Ignore("Argument validation not implemented yet.")]
public void ConstructorRequiresTheCredential()
{
var endpoint = new Uri("http://localhost");

Assert.Throws<ArgumentNullException>(() => new FormLayoutClient(endpoint, null));
Assert.Throws<ArgumentNullException>(() => new FormLayoutClient(endpoint, null, new FormRecognizerClientOptions()));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient"/> constructors.
/// </summary>
[Test]
[Ignore("Argument validation not implemented yet.")]
public void ConstructorRequiresTheOptions()
{
var endpoint = new Uri("http://localhost");
var credential = new FormRecognizerApiKeyCredential("key");

Assert.Throws<ArgumentNullException>(() => new FormLayoutClient(endpoint, credential, null));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient.StartExtractLayoutsAsync(Stream, ContentType, CancellationToken)"/>
/// method.
/// </summary>
[Test]
[Ignore("Argument validation not implemented yet.")]
public void StartExtractLayoutsWithStreamRequiresTheStream()
{
var client = CreateInstrumentedClient();
Assert.ThrowsAsync<ArgumentNullException>(async () => await client.StartExtractLayoutsAsync(null, ContentType.Jpeg));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient.StartExtractLayoutsAsync(Stream, ContentType, CancellationToken)"/>
/// method.
/// </summary>
[Test]
public void StartExtractLayoutsWithStreamRespectsTheCancellationToken()
{
var client = CreateInstrumentedClient();

using var stream = new MemoryStream(Array.Empty<byte>());
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();

Assert.ThrowsAsync<TaskCanceledException>(async () => await client.StartExtractLayoutsAsync(stream, ContentType.Jpeg, cancellationSource.Token));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient.StartExtractLayoutsAsync(Uri, CancellationToken)"/>
/// method.
/// </summary>
[Test]
[Ignore("Argument validation not implemented yet.")]
public void StartExtractLayoutsWithUriRequiresTheUri()
{
var client = CreateInstrumentedClient();
Assert.ThrowsAsync<ArgumentNullException>(async () => await client.StartExtractLayoutsAsync(null));
}

/// <summary>
/// Verifies functionality of the <see cref="FormLayoutClient.StartExtractLayoutsAsync(Uri, CancellationToken)"/>
/// method.
/// </summary>
[Test]
public void StartExtractReceiptsWithUriRespectsTheCancellationToken()
{
var client = CreateInstrumentedClient();
var fakeUri = new Uri("http://localhost");

using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();

Assert.ThrowsAsync<TaskCanceledException>(async () => await client.StartExtractLayoutsAsync(fakeUri, cancellationSource.Token));
}

/// <summary>
/// Creates a fake <see cref="FormLayoutClient" /> and instruments it to make use of the Azure Core
/// Test Framework functionalities.
/// </summary>
/// <returns>The instrumented <see cref="FormLayoutClient" />.</returns>
private FormLayoutClient CreateInstrumentedClient()
{
var fakeEndpoint = new Uri("http://localhost");
var fakeCredential = new FormRecognizerApiKeyCredential("fakeKey");
var client = new FormLayoutClient(fakeEndpoint, fakeCredential);

return InstrumentClient(client);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.IO;

namespace Azure.AI.FormRecognizer.Tests
{
/// <summary>
/// A helper class used to retrieve information to be used for tests.
/// </summary>
public static class TestEnvironment
{
/// <summary>The name of the environment variable from which the Form Recognizer resource's endpoint will be extracted for the live tests.</summary>
public const string EndpointEnvironmentVariableName = "FORM_RECOGNIZER_ENDPOINT";

/// <summary>The name of the environment variable from which the Form Recognizer resource's API key will be extracted for the live tests.</summary>
public const string ApiKeyEnvironmentVariableName = "FORM_RECOGNIZER_API_KEY";

/// <summary>The name of the folder in which test assets are stored.</summary>
private const string AssetsFolderName = "Assets";

/// <summary>The name of the JPG file which contains the receipt to be used for tests.</summary>
private const string ReceiptFilename = "contoso-receipt.jpg";

/// <summary>The format to generate the filenames of the PDF forms to be used for tests.</summary>
private const string InvoiceFilenameFormat = "Invoice_{0}.pdf";

/// <summary>The format to generate the GitHub URIs of the files to be used for tests.</summary>
private const string FileUriFormat = "https://raw.githubusercontent.com/Azure/azure-sdk-for-net/master/sdk/formrecognizer/Azure.AI.FormRecognizer/tests/{0}/{1}";

/// <summary>
/// The relative path to the JPG file which contains the receipt to be used for tests.
/// </summary>
/// <value>The relative path to the JPG file.</value>
public static string ReceiptPath => Path.Combine(AssetsFolderName, ReceiptFilename);

/// <summary>
/// The URI string to the JPG file which contains the receipt to be used for tests.
/// </summary>
/// <value>The URI string to the JPG file.</value>
public static string ReceiptUri => string.Format(FileUriFormat, AssetsFolderName, ReceiptFilename);

/// <summary>
/// Retrieves the relative path to a PDF form available in the test assets.
/// </summary>
/// <param name="index">The index to specify the form to be retrieved.</param>
/// <returns>The relative path to the PDF form corresponding to the specified index.</returns>
public static string RetrieveInvoicePath(int index)
{
var filename = string.Format(InvoiceFilenameFormat, index);
return Path.Combine(AssetsFolderName, filename);
}

/// <summary>
/// Retrieves the URI string to a PDF form available in the test assets.
/// </summary>
/// <param name="index">The index to specify the form to be retrieved.</param>
/// <returns>The URI string to the PDF form corresponding to the specified index.</returns>
public static string RetrieveInvoiceUri(int index)
{
var filename = string.Format(InvoiceFilenameFormat, index);
return string.Format(FileUriFormat, AssetsFolderName, filename);
}
}
}
Loading

0 comments on commit 66c6b39

Please sign in to comment.