Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"commands": [
"nbgv"
]
},
"dotnet-reportgenerator-globaltool": {
"version": "5.5.10",
"commands": [
"reportgenerator"
]
}
}
}
37 changes: 36 additions & 1 deletion .github/workflows/validate-and-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,44 @@ jobs:
--no-restore
--logger "trx;LogFileName=test-results.trx"
--results-directory TestResults
--collect "Code coverage"
--collect "XPlat Code Coverage"
shell: pwsh

- name: Generate code coverage report
if: always()
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'

$coverageFiles = Get-ChildItem -Path TestResults -Recurse -Filter 'coverage.cobertura.xml' -ErrorAction SilentlyContinue
if (-not $coverageFiles) {
Write-Host '::warning::No coverage files found; skipping coverage report.'
return
}

$reports = ($coverageFiles.FullName -join ';')
dotnet tool run reportgenerator `
"-reports:$reports" `
"-targetdir:TestResults/coverage" `
"-reporttypes:Html;MarkdownSummaryGithub;TextSummary" `
"-title:CosmosDBShell Code Coverage"
if ($LASTEXITCODE -ne 0) {
throw "reportgenerator failed with exit code $LASTEXITCODE."
}

$markdown = 'TestResults/coverage/SummaryGithub.md'
if (Test-Path $markdown) {
Get-Content $markdown | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
}

- name: Upload code coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: TestResults/coverage
if-no-files-found: ignore

- name: Run fuzzer smoke test
working-directory: CosmosDBShell.Fuzzer
run: dotnet run --configuration $env:BUILD_CONFIGURATION --no-build --no-restore -- --all
Expand Down
18 changes: 18 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@
]
},
"problemMatcher": "$msCompile"
},
{
"label": "coverage",
"detail": "Run unit tests with code coverage and generate a per-namespace report.",
"command": "pwsh",
"type": "process",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/tools/coverage.ps1"
],
"windows": {
"command": "powershell"
},
"group": "test",
"problemMatcher": []
}
]
}
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ There are several ways you can contribute to the CosmosDBShell project:
- Restore dependencies: `dotnet restore CosmosDBShell.sln`
- Build: `dotnet build CosmosDBShell.sln` (or use the VS Code build task with Ctrl+Shift+B).
- Run tests: `dotnet test CosmosDBShell.sln`
- Run tests with code coverage: `./tools/coverage.ps1` (or the VS Code `coverage` task). Generates an HTML report under `TestResults/coverage` and prints a per-namespace summary; use the report's "Group by" selector for namespace-level coverage.
- Run the tool locally: `dotnet run --project CosmosDBShell/CosmosDBShell.csproj`
- GitHub Actions runs CI and uploads NuGet package artifacts from [.github/workflows/validate-and-package.yml](.github/workflows/validate-and-package.yml).
- Local builds and GitHub Actions use the default NuGet sources (nuget.org). The Azure DevOps pipeline uses [.pipelines/nuget.config](.pipelines/nuget.config) to restrict restores to the internal feed.
Expand Down
120 changes: 120 additions & 0 deletions CosmosDBShell.Tests/CommandTests/BucketCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.CommandTests;

using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.States;
using Azure.Data.Cosmos.Shell.Util;
using Microsoft.Azure.Cosmos;

/// <summary>
/// Unit tests for <see cref="BucketCommand"/>. Covers the pure validation helper and
/// the offline state visitors that do not require a live Cosmos DB connection.
/// </summary>
public class BucketCommandTests
{
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(5)]
public void CheckBucket_WithinRange_ReturnsTrue(int bucket)
{
Assert.True(BucketCommand.CheckBucket(bucket));
}

[Theory]
[InlineData(-1)]
[InlineData(6)]
[InlineData(99)]
public void CheckBucket_OutOfRange_ReturnsFalse(int bucket)
{
Assert.False(BucketCommand.CheckBucket(bucket));
}

[Fact]
public async Task ExecuteAsync_Disconnected_ThrowsNotConnected()
{
using var shell = ShellInterpreter.CreateInstance();
shell.State = new DisconnectedState();
var command = new BucketCommand { Bucket = 3 };

await Assert.ThrowsAsync<NotConnectedException>(
() => command.ExecuteAsync(shell, new CommandState(), "bucket 3", CancellationToken.None));
}

[Fact]
public async Task ExecuteAsync_Connected_ThrowsNotInDatabase()
{
using var shell = ShellInterpreter.CreateInstance();
shell.State = new ConnectedState(CreateTestClient());
var command = new BucketCommand { Bucket = 3 };

await Assert.ThrowsAsync<NotInDatabaseException>(
() => command.ExecuteAsync(shell, new CommandState(), "bucket 3", CancellationToken.None));
}

[Fact]
public async Task ExecuteAsync_InDatabase_SetsBucket()
{
var client = CreateTestClient();
using var shell = ShellInterpreter.CreateInstance();
shell.State = new DatabaseState("TestDatabase", client);
var command = new BucketCommand { Bucket = 3 };

var state = await command.ExecuteAsync(shell, new CommandState(), "bucket 3", CancellationToken.None);

Assert.False(state.IsError);
Assert.Equal(3, client.ClientOptions.ThroughputBucket);
}

[Fact]
public async Task ExecuteAsync_InDatabase_ZeroResetsBucket()
{
var client = CreateTestClient();
client.ClientOptions.ThroughputBucket = 4;
using var shell = ShellInterpreter.CreateInstance();
shell.State = new DatabaseState("TestDatabase", client);
var command = new BucketCommand { Bucket = 0 };

var state = await command.ExecuteAsync(shell, new CommandState(), "bucket 0", CancellationToken.None);

Assert.False(state.IsError);
Assert.Null(client.ClientOptions.ThroughputBucket);
}

[Fact]
public async Task ExecuteAsync_InContainer_NoArgs_ShowsCurrent()
{
var client = CreateTestClient();
using var shell = ShellInterpreter.CreateInstance();
shell.State = new ContainerState("TestContainer", "TestDatabase", client);
var command = new BucketCommand();

var state = await command.ExecuteAsync(shell, new CommandState(), "bucket", CancellationToken.None);

Assert.False(state.IsError);
}

[Fact]
public async Task ExecuteAsync_InDatabase_InvalidValue_ReturnsEmptyState()
{
var client = CreateTestClient();
using var shell = ShellInterpreter.CreateInstance();
shell.State = new DatabaseState("TestDatabase", client);
var command = new BucketCommand { Bucket = 99 };

var state = await command.ExecuteAsync(shell, new CommandState(), "bucket 99", CancellationToken.None);

Assert.False(state.IsError);
Assert.Null(client.ClientOptions.ThroughputBucket);
}

private static CosmosClient CreateTestClient()
{
var connectionString = ParsedDocDBConnectionString.BuildEmulatorConnectionString("https://localhost:8081/");
return new CosmosClient(connectionString);
}
}
97 changes: 97 additions & 0 deletions CosmosDBShell.Tests/CommandTests/CatCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.CommandTests;

using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;
using Azure.Data.Cosmos.Shell.States;

/// <summary>
/// Unit tests for <see cref="CatCommand"/>. The command reads a file from disk and
/// returns its contents as a <see cref="ShellText"/> result, throwing when the file
/// path is missing or does not exist. These paths run without a Cosmos DB connection.
/// </summary>
public class CatCommandTests : IDisposable
{
private readonly string tempFile;

public CatCommandTests()
{
this.tempFile = Path.Combine(Path.GetTempPath(), $"catcmd_{Guid.NewGuid():N}.txt");
}

public void Dispose()
{
if (File.Exists(this.tempFile))
{
File.Delete(this.tempFile);
}

GC.SuppressFinalize(this);
}

[Fact]
public async Task ExecuteAsync_ExistingFile_ReturnsContentsAsShellText()
{
const string contents = "{ \"hello\": \"world\" }\nsecond line";
await File.WriteAllTextAsync(this.tempFile, contents, TestContext.Current.CancellationToken);

using var shell = ShellInterpreter.CreateInstance();
var command = new CatCommand { FilePath = this.tempFile };

var state = await command.ExecuteAsync(shell, new CommandState(), "cat", TestContext.Current.CancellationToken);

var text = Assert.IsType<ShellText>(state.Result);
Assert.Equal(contents, text.Text);
}

[Fact]
public async Task ExecuteAsync_EmptyFile_ReturnsEmptyShellText()
{
await File.WriteAllTextAsync(this.tempFile, string.Empty, TestContext.Current.CancellationToken);

using var shell = ShellInterpreter.CreateInstance();
var command = new CatCommand { FilePath = this.tempFile };

var state = await command.ExecuteAsync(shell, new CommandState(), "cat", TestContext.Current.CancellationToken);

var text = Assert.IsType<ShellText>(state.Result);
Assert.Equal(string.Empty, text.Text);
}

[Fact]
public async Task ExecuteAsync_NonExistentFile_ThrowsCommandException()
{
using var shell = ShellInterpreter.CreateInstance();
var command = new CatCommand { FilePath = this.tempFile };

await Assert.ThrowsAsync<CommandException>(
() => command.ExecuteAsync(shell, new CommandState(), "cat", CancellationToken.None));
}

[Fact]
public async Task ExecuteAsync_NullPath_ThrowsCommandException()
{
using var shell = ShellInterpreter.CreateInstance();
var command = new CatCommand { FilePath = null };

await Assert.ThrowsAsync<CommandException>(
() => command.ExecuteAsync(shell, new CommandState(), "cat", CancellationToken.None));
}

[Fact]
public async Task StateVisitors_ReturnZero()
{
using var shell = ShellInterpreter.CreateInstance();
var command = new CatCommand();

var ct = TestContext.Current.CancellationToken;
Assert.Equal(0, await command.VisitConnectedStateAsync(null!, string.Empty, ct));
Assert.Equal(0, await command.VisitContainerStateAsync(null!, string.Empty, ct));
Assert.Equal(0, await command.VisitDatabaseStateAsync(null!, string.Empty, ct));
Assert.Equal(0, await command.VisitDisconnectedStateAsync(null!, string.Empty, ct));
}
}
Loading
Loading