Skip to content
Open
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
37 changes: 32 additions & 5 deletions .github/workflows/validate-and-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:
**/*.csproj

- name: Restore GitVersion tool
if: github.event_name != 'pull_request'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Expand All @@ -82,6 +83,7 @@ jobs:
}

- name: Compute version properties
if: github.event_name != 'pull_request'
id: version
shell: pwsh
Comment thread
mkrueger marked this conversation as resolved.
run: |
Expand Down Expand Up @@ -140,9 +142,9 @@ jobs:
dotnet build CosmosDBShell.sln
--configuration $env:BUILD_CONFIGURATION
--no-restore
/p:Version=${{ steps.version.outputs.assembly_version }}
/p:FileVersion=${{ steps.version.outputs.file_version }}
/p:InformationalVersion=${{ steps.version.outputs.informational_version }}
$(if ('${{ steps.version.outputs.assembly_version }}') { "/p:Version=${{ steps.version.outputs.assembly_version }}" })
$(if ('${{ steps.version.outputs.file_version }}') { "/p:FileVersion=${{ steps.version.outputs.file_version }}" })
$(if ('${{ steps.version.outputs.informational_version }}') { "/p:InformationalVersion=${{ steps.version.outputs.informational_version }}" })
shell: pwsh

- name: Test solution
Expand Down Expand Up @@ -187,6 +189,7 @@ jobs:
if-no-files-found: ignore

- name: Publish runtime artifacts
if: github.event_name != 'pull_request'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -214,6 +217,7 @@ jobs:
}

- name: Pack NuGet artifacts
if: github.event_name != 'pull_request'
shell: pwsh
run: |
New-Item -ItemType Directory -Path out/nupkg -Force | Out-Null
Expand All @@ -232,6 +236,7 @@ jobs:
/p:ContinuousIntegrationBuild=true

- name: Validate NuGet package set
if: github.event_name != 'pull_request'
shell: pwsh
run: |
$pkgDir = Join-Path $pwd 'out/nupkg'
Expand All @@ -257,9 +262,9 @@ jobs:
$_.Name -notmatch '^CosmosDBShell\.(win-x64|win-arm64|linux-x64|linux-arm64|osx-x64|osx-arm64)\..+\.nupkg$'
}

if (-not $pointerPackages -or $pointerPackages.Count -ne 1) {
if ($pointerPackages -and $pointerPackages.Count -gt 1) {
$names = @($pointerPackages | ForEach-Object { $_.Name })
Write-Error "Expected exactly one pointer package (non-RID). Found: $($names -join ', ')"
Write-Error "Expected at most one pointer package (non-RID). Found: $($names -join ', ')"
exit 1
}

Expand All @@ -270,14 +275,30 @@ jobs:
exit 1
}

- name: Validate pointer package is available for upload
if: github.event_name != 'pull_request'
shell: pwsh
run: |
$pkgDir = Join-Path $pwd 'out/nupkg'
$pointerPackages = Get-ChildItem -Path (Join-Path $pkgDir 'CosmosDBShell.*.nupkg') -ErrorAction SilentlyContinue | Where-Object {
$_.Name -notmatch '^CosmosDBShell\.(win-x64|win-arm64|linux-x64|linux-arm64|osx-x64|osx-arm64)\..+\.nupkg$'
}

if (-not $pointerPackages) {
Write-Error 'Expected a pointer package in out/nupkg, but none was found.'
exit 1
}

- name: List packaged NuGet files
if: github.event_name != 'pull_request'
shell: pwsh
run: |
Get-ChildItem -Path out/nupkg -Filter *.nupkg -File | Sort-Object Name | ForEach-Object {
Write-Host " - $($_.Name) [$($_.Length) bytes]"
}

- name: Write package install summary
if: github.event_name != 'pull_request'
shell: pwsh
run: |
$summary = $env:GITHUB_STEP_SUMMARY
Expand Down Expand Up @@ -308,6 +329,7 @@ jobs:
$lines -join "`n" | Out-File -FilePath $summary -Encoding utf8 -Append

- name: Upload pointer package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-pointer-${{ steps.version.outputs.artifact_suffix }}
Expand All @@ -322,6 +344,7 @@ jobs:
if-no-files-found: error

- name: Upload win-x64 package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-win-x64-${{ steps.version.outputs.artifact_suffix }}
Expand All @@ -336,27 +359,31 @@ jobs:
if-no-files-found: error

- name: Upload linux-x64 package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-linux-x64-${{ steps.version.outputs.artifact_suffix }}
path: out/nupkg/CosmosDBShell.linux-x64.*.nupkg
if-no-files-found: error

- name: Upload linux-arm64 package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-linux-arm64-${{ steps.version.outputs.artifact_suffix }}
path: out/nupkg/CosmosDBShell.linux-arm64.*.nupkg
if-no-files-found: error

- name: Upload osx-x64 package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-osx-x64-${{ steps.version.outputs.artifact_suffix }}
path: out/nupkg/CosmosDBShell.osx-x64.*.nupkg
if-no-files-found: error

- name: Upload osx-arm64 package
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: CosmosDBShell-osx-arm64-${{ steps.version.outputs.artifact_suffix }}
Expand Down
39 changes: 39 additions & 0 deletions .pipelines/CosmosDB-Shell-Official.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,45 @@ extends:
files_to_sign: "**/*.exe;**/*.dll"
search_root: '$(Build.SourcesDirectory)\out'

- task: PowerShell@2
displayName: "Zip signed RID publish folders"
condition: succeeded()
inputs:
targetType: inline
pwsh: true
script: |
$ErrorActionPreference = 'Stop'
$version = "$(CosmosDBShell_PackageVersion)"
$outDir = "$(Build.SourcesDirectory)\out"
$zipDir = Join-Path $outDir 'zip'
New-Item -ItemType Directory -Path $zipDir -Force | Out-Null

$rids = @('win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
foreach ($rid in $rids) {
$ridDir = Join-Path $outDir $rid
if (-not (Test-Path $ridDir)) {
Write-Warning "Skipping $rid — publish folder not found at $ridDir."
continue
}

$zipPath = Join-Path $zipDir ("cosmosdbshell_{0}_{1}.zip" -f $rid, $version)
if (Test-Path $zipPath) {
Remove-Item -Path $zipPath -Force
}

Write-Host "Creating $zipPath"
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$ridDir,
$zipPath,
[System.IO.Compression.CompressionLevel]::Optimal,
$false)
}

Write-Host "Generated archives:"
Get-ChildItem -Path $zipDir -Filter *.zip | Sort-Object Name | ForEach-Object {
Write-Host " - $($_.Name) [$($_.Length) bytes]"
}

- task: DotNetCoreCLI@2
displayName: "Build Fuzzer"
inputs:
Expand Down
171 changes: 171 additions & 0 deletions CosmosDBShell.Tests/CommandTests/FilterCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.CommandTests;

using System.Text.Json;
using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;

public class FilterCommandTests
{
[Fact]
public async Task ExecuteAsync_AppliesPathExpression_AndPreservesStructuredResult()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new
{
items = new[]
{
new { id = "1", status = "active" },
new { id = "2", status = "inactive" },
},
})),
};

var command = new FilterCommand
{
ExpressionText = ".items[0].id",
};

var result = await command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None);

Assert.Same(state, result);
Assert.False(result.IsPrinted);
var json = Assert.IsType<ShellJson>(result.Result);
Assert.Equal("1", json.Value.GetString());
}

[Fact]
public async Task ExecuteAsync_AppliesMapProjection()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new
{
items = new[]
{
new { id = "1", status = "active" },
new { id = "2", status = "inactive" },
},
})),
};

var command = new FilterCommand
{
ExpressionText = ".items | map({id, status})",
};

var result = await command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None);

var json = Assert.IsType<ShellJson>(result.Result);
Assert.Equal(JsonValueKind.Array, json.Value.ValueKind);
Assert.Equal("1", json.Value[0].GetProperty("id").GetString());
Assert.Equal("active", json.Value[0].GetProperty("status").GetString());
Assert.Equal("2", json.Value[1].GetProperty("id").GetString());
}

[Fact]
public async Task ExecuteAsync_NormalizesSequenceResult_ToJsonArray()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new
{
items = new[]
{
new { id = "1" },
new { id = "2" },
},
})),
};

var command = new FilterCommand
{
ExpressionText = ".items[] | .id",
};

var result = await command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None);

var json = Assert.IsType<ShellJson>(result.Result);
Assert.Equal(JsonValueKind.Array, json.Value.ValueKind);
Assert.Equal("1", json.Value[0].GetString());
Assert.Equal("2", json.Value[1].GetString());
}

[Fact]
public async Task ExecuteAsync_NormalizesTextResult_ToJsonString()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new { id = "1" })),
};

var command = new FilterCommand
{
ExpressionText = "type",
};

var result = await command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None);

var json = Assert.IsType<ShellJson>(result.Result);
Assert.Equal(JsonValueKind.String, json.Value.ValueKind);
Assert.Equal("object", json.Value.GetString());
}

[Fact]
public async Task ExecuteAsync_NormalizesBooleanResult_ToJsonBoolean()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new { id = "1" })),
};

var command = new FilterCommand
{
ExpressionText = "true",
};

var result = await command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None);

var json = Assert.IsType<ShellJson>(result.Result);
Assert.Equal(JsonValueKind.True, json.Value.ValueKind);
}

[Fact]
public async Task ExecuteAsync_ThrowsWhenInputMissing()
{
var shell = ShellInterpreter.CreateInstance();
var command = new FilterCommand
{
ExpressionText = ".items",
};

var ex = await Assert.ThrowsAsync<CommandException>(() => command.ExecuteAsync(shell, new CommandState(), string.Empty, CancellationToken.None));
Assert.Contains("requires piped JSON input", ex.Message, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task ExecuteAsync_ThrowsWhenExpressionInvalid()
{
var shell = ShellInterpreter.CreateInstance();
var state = new CommandState
{
Result = new ShellJson(JsonSerializer.SerializeToElement(new { items = new[] { 1, 2 } })),
};
var command = new FilterCommand
{
ExpressionText = ".items[",
};

await Assert.ThrowsAsync<CommandException>(() => command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None));
}
}
Loading
Loading