Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
45 changes: 45 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@ on:
description: 'Runner'

jobs:
test:
name: test (dotnet)
# The ReSharper test framework targets net472 and needs the .NET Framework
# runtime, so these tests cannot run on the ubuntu runner used by `build`.
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: checkout
uses: actions/checkout@v7

- name: install .NET SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: '9.x'

# The ReSharper SDK drops its own NUnit3.TestAdapter.dll into the output
# directory and it cannot be loaded by VSTest, shadowing the working one
# from the NUnit3TestAdapter package. Point VSTest straight at the latter.
- name: restore
run: dotnet restore src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj

- name: test
shell: pwsh
run: |
$packages = if ($env:NUGET_PACKAGES) { $env:NUGET_PACKAGES } else { Join-Path $env:USERPROFILE ".nuget\packages" }
$adapter = Join-Path $packages "nunit3testadapter\4.6.0\build\net462"
if (-not (Test-Path $adapter)) { throw "NUnit adapter not found at $adapter" }
dotnet test src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj `
--test-adapter-path $adapter `
--logger GitHubActions `
--logger "trx;LogFileName=results.trx" `
--no-restore `
--results-directory "${{ github.workspace }}/testresults"

# VSTest exits 0 when it discovers nothing, so a broken adapter would
# otherwise report a green build that ran no tests at all.
- name: assert tests actually ran
shell: pwsh
run: |
$trx = Get-ChildItem "${{ github.workspace }}/testresults" -Filter *.trx -Recurse | Select-Object -First 1
if (-not $trx) { throw "No .trx produced - the test run did not happen." }
$counters = ([xml](Get-Content $trx.FullName)).TestRun.ResultSummary.Counters
Write-Host "total=$($counters.total) executed=$($counters.executed) passed=$($counters.passed) failed=$($counters.failed)"
if ([int]$counters.executed -lt 1) { throw "No tests were executed." }

build:
runs-on: ${{ inputs.runs_on || 'ubuntu-latest' }}
steps:
Expand Down
41 changes: 41 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# AGENTS.md

ReSharper + Rider plugin for MediatR/mediator codebases. Two halves that ship together:
a .NET backend (`src/dotnet`) and a Kotlin/IntelliJ frontend (`src/rider`), wired over a
generated protocol (`protocol/`).

## Layout

| Path | What it is |
| --- | --- |
| `src/dotnet/MediatorPlugin` | Backend: actions, navigations, diagnostics, services |
| `src/dotnet/MediatorPlugin.Tests` | Backend tests (ReSharper test framework) |
| `src/rider/main` | Kotlin frontend + `META-INF/plugin.xml` |
| `protocol` | RD protocol model shared by both halves |
| `test-solution` | Sample solution the tests and manual runs operate on |

## Build and test

```bash
dotnet test src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj
./gradlew :prepareSandbox -PPluginVersion=<version> # full plugin build
```

- **Everything targets `net472`.** The ReSharper test framework needs the .NET Framework
runtime, so the backend tests only run on Windows — CI runs them on `windows-latest`
while the Gradle build runs on Ubuntu. Don't "fix" a test failure that is really a
platform mismatch.
- `MediatorPlugin` has **two project files** in the same directory:
`MediatorPlugin.csproj` (ReSharper) and `MediatorPlugin.Rider.csproj` (Rider). A change
to one usually belongs in both; check before assuming a single build covers you.
- `PluginVersion` in `gradle.properties` and the `<version>` in `plugin.xml` are
`_PLACEHOLDER_` on purpose. CI stamps them via `update-version.sh`. Never commit a
concrete version there.

## File editing

- Modify files with your editor/patch tools (str-replace, create-file, apply-patch).
- Do not write files through the shell: no heredocs (`cat > file <<'EOF'`), no `echo >`,
`printf >`, `tee`, or `sed -i`. Shell redirection into tracked files is prohibited.
- The shell is for read-only inspection (`cat`, `grep`, `ls`, `find`) and for running
builds, tests, and git commands.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.ReSharper.TestFramework;

namespace Octelys.MediatorPlugin.Tests.Infrastructure;

public sealed class MediatRV12TestPackagesAttribute : TestPackagesAttribute
{
public MediatRV12TestPackagesAttribute() : base(Tests.Packages.MediatR.V12)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.ReSharper.TestFramework;

namespace Octelys.MediatorPlugin.Tests.Infrastructure;

public sealed class MediatRV13TestPackagesAttribute : TestPackagesAttribute
{
public MediatRV13TestPackagesAttribute() : base(Tests.Packages.MediatR.V13)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.ReSharper.TestFramework;

namespace Octelys.MediatorPlugin.Tests.Infrastructure;

public sealed class MediatRV14TestPackagesAttribute : TestPackagesAttribute
{
public MediatRV14TestPackagesAttribute() : base(Tests.Packages.MediatR.V14)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.ReSharper.TestFramework;

namespace Octelys.MediatorPlugin.Tests.Infrastructure;

public sealed class MediatorV3TestPackagesAttribute : TestPackagesAttribute
{
public MediatorV3TestPackagesAttribute() : base(Tests.Packages.Mediator.V3)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Octelys.MediatorPlugin.Tests.Infrastructure;

/// <summary>
/// Enumerates every file under a library's folder in the shared TestSolution test data (plus the
/// common Entities folder), so lookup tests can load the whole library instead of curating a file
/// subset per test.
/// </summary>
public static class TestSolutionFiles
{
private static readonly string TestSolutionPath = ResolveTestSolutionPath();

public static string[] MediatR { get; } = LibraryFiles("MediatR");

public static string[] Mediator { get; } = LibraryFiles("Mediator");

private static string[] LibraryFiles
(
string libraryFolderName
)
{
return new[] { libraryFolderName, "Entities" }
.SelectMany(EnumerateRelativeFiles)
.OrderBy(relativePath => relativePath)
.ToArray();
}

private static IEnumerable<string> EnumerateRelativeFiles
(
string folderName
)
{
string folderPath = Path.Combine(TestSolutionPath, folderName);

return Directory
.EnumerateFiles(folderPath, "*.cs", SearchOption.AllDirectories)
.Select(filePath => filePath.Substring(TestSolutionPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'));
}

private static string ResolveTestSolutionPath
(
[CallerFilePath] string sourceFilePath = ""
)
{
string projectPath = Path.GetDirectoryName(Path.GetDirectoryName(sourceFilePath));

return Path.Combine(projectPath!, "test", "data", "TestSolution");
}
}
146 changes: 146 additions & 0 deletions src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Lifetimes;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.Files;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Resources.Shell;
using JetBrains.ReSharper.TestFramework;
using NUnit.Framework;
using ReSharper.MediatorPlugin.Services.Libraries;

namespace Octelys.MediatorPlugin.Tests;

/// <summary>
/// Shared plumbing for tests that load a small test solution and assert which handlers the
/// plugin resolves for a given request or notification type. Test data lives under
/// test/data/TestSolution, organized per library (MediatR, Mediator) and kind (Requests,
/// Notifications, Handlers, Entities).
/// </summary>
public abstract class LookupTestBase : BaseTestWithSingleProject
{
private string _requestTypeName;
private string[] _expectedHandlerNames;

protected override string RelativeTestDataPath => "TestSolution";

/// <summary>
/// Runs inside the loaded test solution: the framework calls this once the project of the
/// current test has been restored and its PSI caches are ready.
/// </summary>
protected override void DoTest
(
Lifetime lifetime,
ISolution solution
)
{
solution.GetPsiServices().Files.CommitAllDocuments();

using (ReadLockCookie.Create())
{
IIdentifier requestIdentifier = FindDeclarationIdentifier(solution, _requestTypeName);

IEnumerable<ITypeElement> handlers = new LibraryAdaptor().FindHandlers(requestIdentifier);

string[] handlerNames = handlers
.Select(handler => handler.ShortName)
.OrderBy(name => name)
.ToArray();

Assert.That(handlerNames, Is.EqualTo(_expectedHandlerNames.OrderBy(name => name).ToArray()));
}
}

/// <summary>
/// Loads the given files as a test solution and asserts that the request or notification
/// resolves to exactly the expected handlers; the lookup itself runs in <see cref="DoTest" />.
/// </summary>
protected void AssertHandlersFor
(
string requestTypeName,
string[] expectedHandlerNames,
params string[] fileNames
)
{
_requestTypeName = requestTypeName;
_expectedHandlerNames = expectedHandlerNames;

DoTestSolution(fileNames);
}

/// <summary>
/// Returns the name token of the type declaration, which is the node the plugin receives
/// when the caret sits on a request in the editor. <paramref name="typeName" /> may be a
/// short name ("GetEntityRequest") when it is unique in the loaded files, or a fully
/// qualified name ("Domain.OtherNamespace.GetEntityRequest") to disambiguate types that
/// share a short name across namespaces.
/// </summary>
private static IIdentifier FindDeclarationIdentifier
(
ISolution solution,
string typeName
)
{
bool isQualified = typeName.Contains('.');

IIdentifier[] candidates = EnumerateIdentifiers(solution)
.Where(candidate => candidate.Parent is IClassLikeDeclaration)
.Where(candidate => isQualified ? GetFullName(candidate) == typeName : candidate.Name == typeName)
.ToArray();

Assert.That
(
candidates,
Has.Length.EqualTo(1),
candidates.Length == 0
? $"No declaration of '{typeName}' was found in the test solution."
: $"Multiple declarations of '{typeName}' were found: {string.Join(", ", candidates.Select(GetFullName))}. " +
"Qualify the name with its namespace to disambiguate."
);

return candidates[0];
}

private static string GetFullName
(
IIdentifier identifier
)
{
IClassLikeDeclaration declaration = (IClassLikeDeclaration)identifier.Parent;

return ((ITypeElement)declaration.DeclaredElement).GetClrName().FullName;
}

private static IEnumerable<IIdentifier> EnumerateIdentifiers
(
ISolution solution
)
{
foreach (IProject project in solution.GetAllProjects())
{
foreach (IProjectFile projectFile in project.GetAllProjectFiles(_ => true))
{
IPsiSourceFile sourceFile = projectFile.ToSourceFile();

if (sourceFile is null)
{
continue;
}

IFile psiFile = sourceFile.GetPrimaryPsiFile();

if (psiFile is null)
{
continue;
}

foreach (IIdentifier identifier in psiFile.Descendants<IIdentifier>().ToEnumerable())
{
yield return identifier;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
using NUnit.Framework;
using ReSharper.MediatorPlugin.Actions;

namespace NoSuchCompany.MediatrPlugin.Tests;
namespace Octelys.MediatorPlugin.Tests;

[TestPackages("MediatR/12.1.0")]
[TestReferences("System.Runtime")] // needed for IReadOnlyCollection type
public class MediatrCreateHandlrContextActionTests : CSharpContextActionExecuteTestBase<CreateHandlrContextAction>
public abstract class MediatRCreateHandlrContextActionTestsBase : CSharpContextActionExecuteTestBase<CreateHandlrContextAction>
{
protected override string RelativeTestDataPath => nameof(MediatrCreateHandlrContextActionTests);
protected override string RelativeTestDataPath => "MediatrCreateHandlrContextActionTests";

protected override string ExtraPath => "";

[Test, Order(0)] public void TestEnvironmentSetupCorrectly() => Assert.Pass();
[Test] public void TestCommand() => DoNamedTest();
[Test] public void TestSimpleQuery() => DoNamedTest();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Octelys.MediatorPlugin.Tests.Infrastructure;

namespace Octelys.MediatorPlugin.Tests;

[MediatRV12TestPackages]
public sealed class MediatRCreateHandlrContextActionTestsV12 : MediatRCreateHandlrContextActionTestsBase
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Octelys.MediatorPlugin.Tests.Infrastructure;

namespace Octelys.MediatorPlugin.Tests;

[MediatRV13TestPackages]
public sealed class MediatRCreateHandlrContextActionTestsV13 : MediatRCreateHandlrContextActionTestsBase
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Octelys.MediatorPlugin.Tests.Infrastructure;

namespace Octelys.MediatorPlugin.Tests;

[MediatRV14TestPackages]
public sealed class MediatRCreateHandlrContextActionTestsV14 : MediatRCreateHandlrContextActionTestsBase
{
}
Loading
Loading