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
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches:
- main
paths:
- 'Alchemy.SourceGenerator/**'
- 'Alchemy.SourceGenerator.Tests/**'
- '.github/workflows/ci.yml'
pull_request:
paths:
- 'Alchemy.SourceGenerator/**'
- 'Alchemy.SourceGenerator.Tests/**'
- '.github/workflows/ci.yml'
workflow_dispatch:

jobs:
source-generator-tests:
name: Source generator tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore Alchemy.SourceGenerator/Alchemy.SourceGenerator.sln

- name: Build
run: >
dotnet build Alchemy.SourceGenerator/Alchemy.SourceGenerator.sln
--configuration Release
--no-restore

- name: Test
run: >
dotnet run
--project Alchemy.SourceGenerator.Tests/Alchemy.SourceGenerator.Tests.csproj
--configuration Release
--no-build
--
--treenode-filter "/*/Alchemy.SourceGenerator.Tests/*/*"
3 changes: 3 additions & 0 deletions Alchemy.SourceGenerator.Tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
obj/
TestResults/
30 changes: 30 additions & 0 deletions Alchemy.SourceGenerator.Tests/Alchemy.SourceGenerator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- TUnit runs on Microsoft.Testing.Platform, so the test project is an executable
and must NOT reference Microsoft.NET.Test.Sdk. -->
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Alchemy.SourceGenerator.Tests</RootNamespace>
<!-- The generator project pins Microsoft.CodeAnalysis 3.9.0; this project deliberately
resolves the newer 4.14.0 to host the generator. The assemblies unify at runtime. -->
<NoWarn>$(NoWarn);NU1608;NU1701</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="TUnit" Version="1.61.38" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
</ItemGroup>

<ItemGroup>
<!-- The generator targets netstandard2.0 against Microsoft.CodeAnalysis 3.9.0.
It is referenced here as a plain library; the newer Roslyn above supplies the
runtime implementation of ISourceGenerator. -->
<ProjectReference Include="..\Alchemy.SourceGenerator\Alchemy.SourceGenerator.csproj" />
</ItemGroup>

</Project>
198 changes: 198 additions & 0 deletions Alchemy.SourceGenerator.Tests/DiagnosticTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using Microsoft.CodeAnalysis;

namespace Alchemy.SourceGenerator.Tests;

/// <summary>
/// The three diagnostics declared in <see cref="DiagnosticDescriptors"/>.
/// </summary>
public class DiagnosticTests
{
[Test]
public async Task ALCHEMY001_is_reported_for_a_non_partial_type()
{
var result = GeneratorUtils.Run("""
using System;
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public class NotPartial
{
[AlchemySerializeField, NonSerialized]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.HasDiagnostic("ALCHEMY001")).IsTrue();

var diagnostic = result.GeneratorDiagnostics.Single(d => d.Id == "ALCHEMY001");
await Assert.That(diagnostic.Severity).IsEqualTo(DiagnosticSeverity.Error);
await Assert.That(diagnostic.GetMessage()).Contains("NotPartial");
}

[Test]
public async Task ALCHEMY001_suppresses_generation_for_that_type()
{
var result = GeneratorUtils.Run("""
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public class NotPartial { }
}
""");

await Assert.That(result.GeneratedSources).IsEmpty();
}

[Test]
public async Task ALCHEMY002_is_reported_for_a_nested_type()
{
var result = GeneratorUtils.Run("""
using System;
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
public partial class Outer
{
[AlchemySerialize]
public partial class Inner
{
[AlchemySerializeField, NonSerialized]
public Dictionary<string, int> map = new();
}
}
}
""");

await Assert.That(result.HasDiagnostic("ALCHEMY002")).IsTrue();
await Assert.That(result.GeneratedSources).IsEmpty();

var diagnostic = result.GeneratorDiagnostics.Single(d => d.Id == "ALCHEMY002");
await Assert.That(diagnostic.Severity).IsEqualTo(DiagnosticSeverity.Error);
await Assert.That(diagnostic.GetMessage()).Contains("Inner");
}

[Test]
public async Task ALCHEMY003_warns_when_the_field_is_not_marked_NonSerialized()
{
// Without [NonSerialized] Unity serializes the field itself as well as the
// JSON payload, so the value is stored twice and can diverge.
var result = GeneratorUtils.Run("""
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public partial class Warned
{
[AlchemySerializeField]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.HasDiagnostic("ALCHEMY003")).IsTrue();

var diagnostic = result.GeneratorDiagnostics.Single(d => d.Id == "ALCHEMY003");
await Assert.That(diagnostic.Severity).IsEqualTo(DiagnosticSeverity.Warning);
await Assert.That(diagnostic.GetMessage()).Contains("map");
}

[Test]
public async Task ALCHEMY003_still_generates_the_code()
{
// It is a warning, not an error — generation must continue.
var result = GeneratorUtils.Run("""
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public partial class Warned
{
[AlchemySerializeField]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.GeneratedSources.Length).IsEqualTo(1);
await Assert.That(result.DescribeCompilationErrors()).IsEqualTo("<none>");
}

[Test]
public async Task No_diagnostic_when_NonSerialized_is_present()
{
var result = GeneratorUtils.Run("""
using System;
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public partial class Clean
{
[AlchemySerializeField, NonSerialized]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.GeneratorDiagnostics).IsEmpty();
}

[Test]
public async Task Namespace_qualified_NonSerialized_is_recognised()
{
// The bare spelling is covered by No_diagnostic_when_NonSerialized_is_present.
var result = GeneratorUtils.Run("""
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public partial class Spelled
{
[AlchemySerializeField, System.NonSerialized]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.HasDiagnostic("ALCHEMY003")).IsFalse();
}

[Test]
public async Task The_generator_never_crashes_on_well_formed_input()
{
// A generator crash surfaces as the catch-all "AlchemySerializeGeneratorError".
var result = GeneratorUtils.Run("""
using System;
using System.Collections.Generic;
using Alchemy.Serialization;

namespace Demo
{
[AlchemySerialize]
public partial class Fine
{
[AlchemySerializeField, NonSerialized]
public Dictionary<string, int> map = new();
}
}
""");

await Assert.That(result.HasDiagnostic("AlchemySerializeGeneratorError")).IsFalse();
}
}
Loading