Skip to content
Draft
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
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
<PackageVersion Include="Websocket.Client" Version="5.5.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.303" />
<!-- Tooling dependencies (CapabilityMatrix generator — unreleased, not packed) -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<!-- Test dependencies -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="MSTest.TestAdapter" Version="4.3.3" />
Expand Down
58 changes: 45 additions & 13 deletions Supabase.sln
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "red-test", "red-test", "{61
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{996F8526-0051-4327-A551-1175C455CC5F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DependencyInjection", "DependencyInjection", "{9FD9C523-BB83-D457-CA1C-9430D9580376}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DependencyInjection", "DependencyInjection", "{C0B9E91A-9577-B879-D937-00DB4C86FF49}"
ProjectSection(SolutionItems) = preProject
packages\extensions\DependencyInjection\README.md = packages\extensions\DependencyInjection\README.md
EndProjectSection
Expand Down Expand Up @@ -241,7 +241,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Quality Gate", "Quality Gat
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Public API", "Public API", "{77474E39-B8A1-46A4-BF35-C042DAFCAE4B}"
EndProject

Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CapabilityMatrix", "CapabilityMatrix", "{287C1647-12FE-4F77-D529-18732EC0C527}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CapabilityMatrix", "packages\CapabilityMatrix\CapabilityMatrix\CapabilityMatrix.csproj", "{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CapabilityMatrix.Tests", "packages\CapabilityMatrix\CapabilityMatrix.Tests\CapabilityMatrix.Tests.csproj", "{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -444,16 +449,35 @@ Global
{709EBF63-BDED-465F-9CEA-D1C7C9E2AB03}.Release|x64.Build.0 = Release|Any CPU
{709EBF63-BDED-465F-9CEA-D1C7C9E2AB03}.Release|x86.ActiveCfg = Release|Any CPU
{709EBF63-BDED-465F-9CEA-D1C7C9E2AB03}.Release|x86.Build.0 = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|x64.ActiveCfg = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|x64.Build.0 = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|x86.ActiveCfg = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Debug|x86.Build.0 = Debug|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|Any CPU.Build.0 = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|x64.ActiveCfg = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|x64.Build.0 = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|x86.ActiveCfg = Release|Any CPU
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8}.Release|x86.Build.0 = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|x64.ActiveCfg = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|x64.Build.0 = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|x86.ActiveCfg = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Debug|x86.Build.0 = Debug|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|Any CPU.Build.0 = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|x64.ActiveCfg = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|x64.Build.0 = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|x86.ActiveCfg = Release|Any CPU
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1085D75D-D916-4F8D-8B9B-8C242DA847BE} = {3074019A-049A-4959-9FCC-410144E423F5}
{320443A0-3589-44BD-85FD-59118B0D8AF4} = {3074019A-049A-4959-9FCC-410144E423F5}
{D40502AB-54A1-4E0B-952B-C96AF567B08A} = {3074019A-049A-4959-9FCC-410144E423F5}
{53322416-5ED2-433A-8CCD-36BC424C7ED0} = {3074019A-049A-4959-9FCC-410144E423F5}
{9BB19953-99BC-4D4C-B164-FAC2444FE56C} = {3074019A-049A-4959-9FCC-410144E423F5}
{81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{52157DD7-2D5D-41C3-87A4-2894AD47528C} = {81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07}
{A1459477-0702-4211-956E-5D2F88BEF669} = {81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07}
Expand All @@ -476,6 +500,8 @@ Global
{B898A942-9C1A-4C3F-8992-664B636D0ED8} = {48B3645E-6A5B-E050-DA5F-480AE4911C13}
{ACE6D9C0-1F55-4A95-8250-C074D5EBA913} = {48B3645E-6A5B-E050-DA5F-480AE4911C13}
{D1072B4D-1697-4401-9431-F90A6B9AD2BB} = {AB28C3BB-F406-476D-9004-804D35EAA048}
{226BEB75-46D1-4366-BBDE-03C9E7A6D6F1} = {B7F880FB-DCBF-4EE7-82F3-C8D2127984F7}
{3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5} = {B7F880FB-DCBF-4EE7-82F3-C8D2127984F7}
{27BCCB1B-2091-4A34-9966-098BCE684A6E} = {3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5}
{A024290F-C0C4-4A3B-BE9D-7715CC9AF747} = {3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5}
{AEA69720-4764-432F-9843-634D152C8C33} = {3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5}
Expand All @@ -485,14 +511,20 @@ Global
{097CDFF6-9B52-46C9-88BF-62ED8FE62957} = {3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5}
{612A3F59-5D51-4E02-80C1-4E687887B73D} = {3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5}
{996F8526-0051-4327-A551-1175C455CC5F} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{9FD9C523-BB83-D457-CA1C-9430D9580376} = {996F8526-0051-4327-A551-1175C455CC5F}
{481455AC-C8A3-4F61-A0C2-5D1CBF3D76F6} = {9FD9C523-BB83-D457-CA1C-9430D9580376}
{709EBF63-BDED-465F-9CEA-D1C7C9E2AB03} = {9FD9C523-BB83-D457-CA1C-9430D9580376}
{C0B9E91A-9577-B879-D937-00DB4C86FF49} = {996F8526-0051-4327-A551-1175C455CC5F}
{481455AC-C8A3-4F61-A0C2-5D1CBF3D76F6} = {C0B9E91A-9577-B879-D937-00DB4C86FF49}
{709EBF63-BDED-465F-9CEA-D1C7C9E2AB03} = {C0B9E91A-9577-B879-D937-00DB4C86FF49}
{3074019A-049A-4959-9FCC-410144E423F5} = {77474E39-B8A1-46A4-BF35-C042DAFCAE4B}
{1085D75D-D916-4F8D-8B9B-8C242DA847BE} = {3074019A-049A-4959-9FCC-410144E423F5}
{320443A0-3589-44BD-85FD-59118B0D8AF4} = {3074019A-049A-4959-9FCC-410144E423F5}
{D40502AB-54A1-4E0B-952B-C96AF567B08A} = {3074019A-049A-4959-9FCC-410144E423F5}
{53322416-5ED2-433A-8CCD-36BC424C7ED0} = {3074019A-049A-4959-9FCC-410144E423F5}
{9BB19953-99BC-4D4C-B164-FAC2444FE56C} = {3074019A-049A-4959-9FCC-410144E423F5}
{B7F880FB-DCBF-4EE7-82F3-C8D2127984F7} = {97E13694-38F0-4D01-BA50-436D9ADCF99C}
{3CC0DE6E-563A-4EA3-AF1A-0AF14FC57FE5} = {B7F880FB-DCBF-4EE7-82F3-C8D2127984F7}
{77474E39-B8A1-46A4-BF35-C042DAFCAE4B} = {97E13694-38F0-4D01-BA50-436D9ADCF99C}
{3074019A-049A-4959-9FCC-410144E423F5} = {77474E39-B8A1-46A4-BF35-C042DAFCAE4B}
{226BEB75-46D1-4366-BBDE-03C9E7A6D6F1} = {B7F880FB-DCBF-4EE7-82F3-C8D2127984F7}
{287C1647-12FE-4F77-D529-18732EC0C527} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{7EA6EBE2-8EAD-4ECC-A866-24B0BEE88FC8} = {287C1647-12FE-4F77-D529-18732EC0C527}
{0C590BBB-BFBF-4D2C-A1EF-CDD3136FE19C} = {287C1647-12FE-4F77-D529-18732EC0C527}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {832DE89D-7252-4B03-9301-BB8D36B40992}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>CapabilityMatrix.Tests</AssemblyName>
<RootNamespace>Supabase.CapabilityMatrix.Tests</RootNamespace>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="MSTest.TestAdapter" />
<PackageReference Include="MSTest.TestFramework" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CapabilityMatrix\CapabilityMatrix.csproj" />
</ItemGroup>

</Project>
84 changes: 84 additions & 0 deletions packages/CapabilityMatrix/CapabilityMatrix.Tests/CliTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using FluentAssertions;

namespace Supabase.CapabilityMatrix.Tests;

[TestClass]
public class CliEntryPointTests
{
private static (int Code, string Out, string Err) Run(params string[] args)
{
var output = new StringWriter();
var error = new StringWriter();
var code = CliEntryPoint.Run(args, output, error);
return (code, output.ToString(), error.ToString());
}

[TestMethod]
public void NoArgumentsPrintsUsage()
{
var (code, _, err) = Run();
code.Should().Be(2);
err.Should().Contain("Usage:");
}

[TestMethod]
public void UnknownCommandFails()
{
var (code, _, err) = Run("frobnicate");
code.Should().Be(2);
err.Should().Contain("Unknown command");
}

[TestMethod]
public void GenerateWithoutSpecFails()
{
var (code, _, err) = Run("generate", "--root", ".");
code.Should().Be(2);
err.Should().Contain("requires --spec");
}

[TestMethod]
public void MissingComplianceFileIsHandledAsAnEnvironmentError()
{
var (code, _, err) = Run("locate", "--root", Path.GetTempPath(), "--compliance", Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.yaml"));
code.Should().Be(2);
err.Should().Contain("❌");
}

[TestMethod]
public void GenerateEndToEndThroughTheFilesystem()
{
var root = Directory.CreateTempSubdirectory("capmatrix");
try
{
var pkg = Directory.CreateDirectory(Path.Combine(root.FullName, "packages", "Gotrue", "Gotrue"));
File.WriteAllText(Path.Combine(pkg.FullName, "Client.cs"),
"""
using Supabase.Core.Attributes;
namespace Supabase.Gotrue
{
public class Client
{
[ImplementedCapability("auth.sign_in.sign_up")]
public void SignUp() { }
}
}
""");

var spec = Directory.CreateDirectory(Path.Combine(root.FullName, "spec"));
File.WriteAllText(Path.Combine(spec.FullName, "auth.yaml"),
"area: auth\ntitle: Authentication\ngroups:\n - id: sign_in\n title: Sign-in\nfeatures:\n - id: auth.sign_in.sign_up\n group: sign_in\n");

var outPath = Path.Combine(root.FullName, "sdk-compliance.yaml");
var (code, output, _) = Run("generate", "--root", root.FullName, "--spec", spec.FullName, "--out", outPath);

code.Should().Be(0);
output.Should().Contain("✅");
File.ReadAllText(outPath).Should().Contain("auth.sign_in.sign_up:").And.Contain("- Client.SignUp");
}
finally
{
root.Delete(recursive: true);
}
}
}
137 changes: 137 additions & 0 deletions packages/CapabilityMatrix/CapabilityMatrix.Tests/CommandRunnerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using FluentAssertions;

namespace Supabase.CapabilityMatrix.Tests;

internal sealed class InMemoryWorkspace : IWorkspace
{
public List<SymbolWalker.SourceFile> Sources { get; } = [];
public List<string> SpecDocuments { get; } = [];
public string ComplianceText { get; set; } = "sdk: csharp\nfeatures:\n";
public string? Written { get; private set; }

public IReadOnlyList<SymbolWalker.SourceFile> ReadSources() => this.Sources;

public IReadOnlyList<string> ReadSpecDocuments() => this.SpecDocuments;

public string ReadComplianceText() => this.ComplianceText;

public void WriteComplianceText(string content) => this.Written = content;
}

[TestClass]
public class CommandRunnerTests
{
private const string Spec =
"""
area: auth
title: Authentication
groups:
- id: sign_in
title: Sign-in
features:
- id: auth.sign_in.sign_up
group: sign_in
""";

private static InMemoryWorkspace WorkspaceWith(string source, string? compliance = null)
{
var workspace = new InMemoryWorkspace();
workspace.Sources.Add(new SymbolWalker.SourceFile("packages/Gotrue/Gotrue/Client.cs", source));
workspace.SpecDocuments.Add(Spec);
if (compliance is not null)
workspace.ComplianceText = compliance;
return workspace;
}

private const string GoodSource =
"""
using Supabase.Core.Attributes;
namespace Supabase.Gotrue
{
public class Client
{
[ImplementedCapability("auth.sign_in.sign_up")]
public void SignUp() { }
}
}
""";

private static (int Code, string Output) Run(Func<CommandRunner, int> act, IWorkspace workspace)
{
var writer = new StringWriter();
var code = act(new CommandRunner(workspace, writer));
return (code, writer.ToString());
}

[TestMethod]
public void GenerateWritesTheFileOnACleanTree()
{
var workspace = WorkspaceWith(GoodSource);
var (code, output) = Run(r => r.Generate(), workspace);

code.Should().Be(CommandRunner.Success);
output.Should().Contain("✅");
workspace.Written.Should().Contain("auth.sign_in.sign_up:").And.Contain("- Client.SignUp");
}

[TestMethod]
public void GenerateFailsAndWritesNothingOnAnUnknownKey()
{
var source = GoodSource.Replace("auth.sign_in.sign_up", "auth.sign_in.typo");
var workspace = WorkspaceWith(source);
var (code, output) = Run(r => r.Generate(), workspace);

code.Should().Be(CommandRunner.Failure);
output.Should().Contain("Cannot generate").And.Contain("auth.sign_in.typo");
workspace.Written.Should().BeNull();
}

[TestMethod]
public void ValidatePassesOnACleanTreeAndFailsOnConflict()
{
Run(r => r.Validate(), WorkspaceWith(GoodSource)).Code.Should().Be(CommandRunner.Success);

var conflicting = GoodSource.Replace(
"[ImplementedCapability(\"auth.sign_in.sign_up\")]",
"[ImplementedCapability(\"auth.sign_in.sign_up\")]\n [PartialCapability(\"auth.sign_in.sign_up\", \"partial\")]");
var (code, output) = Run(r => r.Validate(), WorkspaceWith(conflicting));
code.Should().Be(CommandRunner.Failure);
output.Should().Contain("conflicting statuses");
}

[TestMethod]
public void CheckPassesWhenCommittedMatchesAndFailsOtherwise()
{
var workspace = WorkspaceWith(GoodSource);
workspace.ComplianceText = YamlEmitter.Emit(
ComplianceModel.Build(SymbolWalker.Extract(workspace.Sources)).Entries,
SpecModel.Parse(workspace.SpecDocuments));
Run(r => r.Check(), workspace).Code.Should().Be(CommandRunner.Success);

var stale = WorkspaceWith(GoodSource, "sdk: csharp\nfeatures:\n auth.sign_in.sign_up: implemented\n");
var (code, output) = Run(r => r.Check(), stale);
code.Should().Be(CommandRunner.Failure);
output.Should().Contain("out of sync").And.Contain("symbols differ");
}

[TestMethod]
public void LocateReportsResolvedAndFailsOnUnresolved()
{
var resolvedCompliance = "sdk: csharp\nfeatures:\n auth.sign_in.sign_up:\n status: implemented\n symbols:\n - Client.SignUp\n";
Run(r => r.Locate(), WorkspaceWith(GoodSource, resolvedCompliance)).Should()
.Match<(int Code, string Output)>(t => t.Code == CommandRunner.Success && t.Output.Contains("1 resolved"));

var missingCompliance = "sdk: csharp\nfeatures:\n auth.sign_in.sign_up:\n status: implemented\n symbols:\n - Client.Ghost\n";
var (code, output) = Run(r => r.Locate(), WorkspaceWith(GoodSource, missingCompliance));
code.Should().Be(CommandRunner.Failure);
output.Should().Contain("MISS").And.Contain("Client.Ghost");
}

[TestMethod]
public void IndexEmitsJsonForTheExtractedSurface()
{
var (code, output) = Run(r => r.Index(), WorkspaceWith(GoodSource));
code.Should().Be(CommandRunner.Success);
output.Should().Contain("\"QualifiedName\": \"Client.SignUp\"").And.Contain("auth.sign_in.sign_up");
}
}
Loading
Loading