В отчетах о покрытии кода обнаружена ветвь метода, возвращающего IAsyncEnumerableC#

Место общения программистов C#
Ответить
Anonymous
 В отчетах о покрытии кода обнаружена ветвь метода, возвращающего IAsyncEnumerable

Сообщение Anonymous »

У меня есть следующая функция, которая по сути является оболочкой чего-то, что выполняет вызов SQL и изменяет результаты:
public async IAsyncEnumerable ExecuteTable(string sql, IDictionary? parameters, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var res = await ExecuteSQL(sql, parameters, cancellationToken).ConfigureAwait(false);
foreach (var row in res.Rows)
{
cancellationToken.ThrowIfCancellationRequested();
yield return RowConverter.ConvertRow(res.Columns, row);
}
}

Вы можете заметить, что перечисление на самом деле не является асинхронным, но это реализация интерфейса, и оно должно возвращать IAsyncEnumerable. ExecuteSQL действительно является асинхронным, но он отправляет запрос в другое место, а затем возвращает результат в одном пакете (в удобной для сети форме, которую необходимо изменить с помощью ConvertRow).
У меня есть модульные тесты, охватывающие это. Тесты делают следующее:
  • Успешное выполнение с результатами.
  • Успешное выполнение без результатов.
  • Отмена в ExecuteSQL (вызывает OperationCanceledException).
  • Отмена в цикле.
  • Исключение, не связанное с отменой в ExecuteSQL.
Вот в чем вопрос: инструмент покрытия кода сообщает о незакрытой ветке в самой последней закрытой скобке (той, которая завершает метод). Почему?
Конечно, в этом нет ничего страшного; Я бы согласился проигнорировать эту проблему, за исключением того, что есть другой метод, который абсолютно идентичен, за исключением того, что он не является универсальным и вместо этого возвращает IAsyncEnumerable (с вызовом RowConverter.CovertDynamic вместо RowConverter.ConvertRow). Этот другой метод применяется модульными тестами точно таким же образом, и покрытие кода сообщает, что он полностью покрыт!
Итак, мне любопытно, в чем может быть разница - почему компилятор, очевидно, создает недоступную ветвь, но только для общей, а не для динамической.
Я даже пробовал отладку в IL, но за этим слишком сложно уследить. Я также создал неуниверсальную и нединамическую версию, просто чтобы посмотреть, что произойдет; он сообщает, что полностью покрыт.
Для справки: здесь используется xUnit и Coverlet с Fine Code Coverage в VS.
Полный код модульного теста следует. Фактические сетевые вызовы высмеиваются. Целью этих тестов является проверка оболочки. Да, для этого существует слишком много тестов; это специально потому, что я хочу узнать, что происходит. Обычно я бы не стал писать столько тестов для пятистрочной функции.
[Fact]
public async Task ExecuteTable_CallsWrapperAndReturnsRecords()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
for (int i = 1; i capturedRequest = request), Arg.Any()).Returns(response);
var client = CreateDatabaseClient();
// Act
var results = new List();
await foreach (var record in client.ExecuteTable("SELECT * FROM Table", null, TestContext.Current.CancellationToken))
{
results.Add(record);
}
// Assert
Assert.NotNull(capturedRequest);
Assert.Equal("SELECT * FROM Table", capturedRequest.Sql);
Assert.Empty(capturedRequest.Parameters);
Assert.Equal(3, results.Count);
for (int i = 0; i < 3; i++)
{
Assert.Equal(i + 1, results.ID);
Assert.Equal($"Test{i + 1}", results.Name);
Assert.Equal(3.14 * (i + 1), results.Value);
}
}

[Fact]
public async Task ExecuteTable_NoResults()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
_databaseClientWrapperMock.ExecuteQuery(Arg.Do(request => capturedRequest = request), Arg.Any()).Returns(response);
var client = CreateDatabaseClient();
// Act
var results = new List();
await foreach (var record in client.ExecuteTable("SELECT * FROM Table", null, TestContext.Current.CancellationToken))
results.Add(record);
// Assert
Assert.NotNull(capturedRequest);
Assert.Equal("SELECT * FROM Table", capturedRequest.Sql);
Assert.Empty(capturedRequest.Parameters);
Assert.Empty(results);
}

[Fact]
public async Task ExecuteTable_Exception()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
await _databaseClientWrapperMock.ExecuteQuery(Arg.Do(request => { capturedRequest = request; throw new Exception(); }), Arg.Any());
var client = CreateDatabaseClient();
// Act & Assert
var results = new List();
await Assert.ThrowsAsync(async () =>
{
await foreach (var record in client.ExecuteTable("SELECT * FROM Table", null, TestContext.Current.CancellationToken))
results.Add(record);
});
// Assert
Assert.NotNull(capturedRequest);
Assert.Equal("SELECT * FROM Table", capturedRequest.Sql);
Assert.Empty(capturedRequest.Parameters);
Assert.Empty(results);
}

[Fact]
public async Task ExecuteTable_SlowExecution()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
var tcs = new TaskCompletionSource();
_databaseClientWrapperMock.ExecuteQuery(Arg.Do(request => capturedRequest = request), Arg.Any()).Returns(async _ => { await tcs.Task.WaitAsync(TestContext.Current.CancellationToken); await Task.Delay(100); return response; });
var client = CreateDatabaseClient();
// Act
var results = new List();
var enumeration = client.ExecuteTable("SELECT * FROM Table", null, TestContext.Current.CancellationToken);
await using var enumerator = enumeration.GetAsyncEnumerator(TestContext.Current.CancellationToken);
tcs.SetResult();
while (await enumerator.MoveNextAsync())
results.Add(enumerator.Current);
// Assert
Assert.NotNull(capturedRequest);
Assert.Equal("SELECT * FROM Table", capturedRequest.Sql);
Assert.Empty(capturedRequest.Parameters);
Assert.Empty(results);
}

[Fact]
public async Task ExecuteTable_CancelImmediately()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
_databaseClientWrapperMock.ExecuteQuery(Arg.Do(request => capturedRequest = request), Arg.Any()).Returns(response);
var client = CreateDatabaseClient();
// Act & Assert
var results = new List();
var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync(async () => { await client.ExecuteTable("SELECT * FROM Table", null, cts.Token).FirstAsync(cts.Token); });
}

[Fact]
public async Task ExecuteTable_CancelEnumeration()
{
// Arrange
QueryRequest? capturedRequest = null;
QueryResponse response = new();
response.Columns.Add(new Column() { Name = "ID", Type = RPCType.Int32 });
response.Columns.Add(new Column() { Name = "Name", Type = RPCType.String });
response.Columns.Add(new Column() { Name = "Value", Type = RPCType.Double });
for (int i = 1; i capturedRequest = request), Arg.Any()).Returns(response);
var client = CreateDatabaseClient();
var cts = new CancellationTokenSource();
// Act
var enumerable = client.ExecuteTable("SELECT * FROM Table", null, TestContext.Current.CancellationToken);
await using var enumerator = enumerable.GetAsyncEnumerator(cts.Token);
bool hasFirstRecord = await enumerator.MoveNextAsync();
var firstRecord = enumerator.Current;
cts.Cancel();
await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
// Assert
Assert.NotNull(capturedRequest);
Assert.Equal("SELECT * FROM Table", capturedRequest.Sql);
Assert.Empty(capturedRequest.Parameters);
Assert.True(hasFirstRecord);
Assert.NotNull(firstRecord);
Assert.Equal(1, firstRecord.ID);
Assert.Equal("Test1", firstRecord.Name);
Assert.Equal(3.14, firstRecord.Value);
}

Изменить: вот отчет о покрытии для ExecuteTable и ExecuteDynamic. Вы заметите, что у ExecuteTable есть ветвь в строке 63 (которая является закрывающей скобкой). Закрывающая скобка в ExecuteDynamic находится в строке 73 и не имеет ответвления! Почему компилятор создает ветвь в закрывающей скобке ExecuteTable, а не в ExecuteDynamic?

















































































Подробнее здесь: https://stackoverflow.com/questions/799 ... numerablet
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»