mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #273 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
This commit is contained in:
commit
aa879d2c9a
@ -1,6 +1,6 @@
|
||||
# GFramework Skills
|
||||
|
||||
文档工作流的公开入口已统一为 `gframework-doc-refresh`。
|
||||
公开入口目前包含 `gframework-doc-refresh` 与 `gframework-batch-boot`。
|
||||
|
||||
## 公开入口
|
||||
|
||||
@ -29,6 +29,30 @@
|
||||
/gframework-doc-refresh Cqrs
|
||||
```
|
||||
|
||||
### `gframework-batch-boot`
|
||||
|
||||
在 `gframework-boot` 的基础上,自动推进可分批执行的重复性任务,不需要人工一轮轮重新触发。
|
||||
|
||||
适用场景:
|
||||
|
||||
- analyzer warning reduction
|
||||
- 大批量测试结构收口
|
||||
- 分模块文档刷新 wave
|
||||
- 任何有明确 stop condition 的多批次任务
|
||||
|
||||
推荐调用:
|
||||
|
||||
```bash
|
||||
/gframework-batch-boot <task-or-stop-condition>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/gframework-batch-boot continue analyzer warning reduction until branch diff vs origin/main approaches 75 files
|
||||
/gframework-batch-boot keep refactoring repetitive source-generator tests in bounded batches
|
||||
```
|
||||
|
||||
## 共享资源
|
||||
|
||||
- `_shared/DOCUMENTATION_STANDARDS.md`
|
||||
|
||||
139
.agents/skills/gframework-batch-boot/SKILL.md
Normal file
139
.agents/skills/gframework-batch-boot/SKILL.md
Normal file
@ -0,0 +1,139 @@
|
||||
---
|
||||
name: gframework-batch-boot
|
||||
description: Repository-specific bulk-task workflow for the GFramework repo. Use when Codex should start from the normal GFramework boot context and then continue a repetitive or large-scope task in automatic batches without waiting for manual round-by-round prompts, especially for analyzer warning cleanup, repetitive test refactors, documentation waves, or similar multi-file work with an explicit stop condition such as changed-file count, warning count, or timebox.
|
||||
---
|
||||
|
||||
# GFramework Batch Boot
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when `gframework-boot` is necessary but not sufficient because the task should keep advancing in bounded
|
||||
batches until a clear stop condition is met.
|
||||
|
||||
Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`; it does not replace it.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
1. Execute the normal `gframework-boot` startup sequence first:
|
||||
- read `AGENTS.md`
|
||||
- read `.ai/environment/tools.ai.yaml`
|
||||
- read `ai-plan/public/README.md`
|
||||
- read the mapped active topic `todos/` and `traces/`
|
||||
2. Classify the task as a batch candidate only if all of the following are true:
|
||||
- the work is repetitive, sliceable, or likely to require multiple similar iterations
|
||||
- each batch can be given an explicit ownership boundary
|
||||
- a stop condition can be measured locally
|
||||
3. Before any delegation, define the batch objective in one sentence:
|
||||
- warning family reduction
|
||||
- repeated test refactor pattern
|
||||
- module-by-module documentation refresh
|
||||
- other repetitive multi-file cleanup
|
||||
|
||||
## Baseline Selection
|
||||
|
||||
When the stop condition depends on branch size or changed-file count, choose the baseline carefully.
|
||||
|
||||
1. Prefer the freshest remote-tracking reference that already exists locally:
|
||||
- `origin/main`
|
||||
- or the mapped upstream base branch for the current topic
|
||||
2. Do not default to local `main` when `refs/heads/main` is behind `refs/remotes/origin/main`.
|
||||
3. If both local and remote-tracking refs exist, report:
|
||||
- ref name
|
||||
- short SHA
|
||||
- committer date
|
||||
4. If only a local branch exists, state that the baseline may be stale before using it.
|
||||
5. When the task is tied to a PR or topic branch rather than `main`, prefer that explicit upstream comparison target over
|
||||
a generic `main`.
|
||||
|
||||
For changed-file limits, measure branch-wide scope against the chosen baseline, not just the current working tree:
|
||||
|
||||
- use `git diff --name-only <baseline>...HEAD`
|
||||
- do not confuse branch diff size with `git status --short`
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Choose one primary stop condition before the first batch and restate it to the user.
|
||||
|
||||
Common stop conditions:
|
||||
|
||||
- branch diff vs baseline approaches a file-count threshold
|
||||
- warnings-only build reaches a target count
|
||||
- a specific hotspot list is exhausted
|
||||
- a timebox or validation budget is reached
|
||||
|
||||
If multiple stop conditions exist, rank them and treat one as primary.
|
||||
|
||||
## Batch Loop
|
||||
|
||||
1. Inspect the current state before the first batch:
|
||||
- current branch and active topic
|
||||
- selected baseline
|
||||
- current stop-condition metric
|
||||
- next candidate slices
|
||||
2. Keep the critical path local.
|
||||
3. Delegate only bounded slices with explicit ownership:
|
||||
- one file
|
||||
- one warning family within one project
|
||||
- one module documentation wave
|
||||
4. For each worker batch, specify:
|
||||
- objective
|
||||
- owned files or subsystem
|
||||
- required validation commands
|
||||
- output format
|
||||
- reminder that other agents may be editing the repo
|
||||
5. While workers run, use the main thread for non-overlapping tasks:
|
||||
- queue the next candidate slice
|
||||
- inspect the next hotspot
|
||||
- recompute branch size or warning distribution
|
||||
6. After each completed batch:
|
||||
- integrate or verify the result
|
||||
- rerun the required validation
|
||||
- recompute the primary stop-condition metric
|
||||
- decide immediately whether to continue or stop
|
||||
7. Do not require the user to manually trigger every round unless:
|
||||
- the next slice is ambiguous
|
||||
- a validation failure changes strategy
|
||||
- the batch objective conflicts with the active topic
|
||||
|
||||
## Task Tracking
|
||||
|
||||
For multi-batch work, keep recovery artifacts current.
|
||||
|
||||
- Update the active `ai-plan/public/<topic>/todos/` document when a meaningful batch lands.
|
||||
- Update the matching `traces/` document with:
|
||||
- accepted delegated scope
|
||||
- validation milestones
|
||||
- current stop-condition metric
|
||||
- next recommended batch
|
||||
- Keep the active recovery point concise; archive detailed history when it starts to sprawl.
|
||||
|
||||
## Delegation Defaults
|
||||
|
||||
- Prefer `worker` subagents for independent write slices.
|
||||
- Prefer `explorer` subagents for read-only hotspot ranking or next-batch discovery.
|
||||
- Keep each worker ownership boundary disjoint.
|
||||
- Avoid launching a new batch when the expected write set would push the branch beyond the declared threshold without a
|
||||
deliberate decision.
|
||||
|
||||
## Completion
|
||||
|
||||
Stop the loop when any of the following becomes true:
|
||||
|
||||
- the primary stop condition has been reached or exceeded
|
||||
- the remaining slices are no longer low-risk
|
||||
- validation failures indicate the task is no longer repetitive
|
||||
- the branch has grown large enough that reviewability would materially degrade
|
||||
|
||||
When stopping, report:
|
||||
|
||||
- which baseline was used
|
||||
- the exact metric value at stop time
|
||||
- completed batches
|
||||
- remaining candidate batches
|
||||
- whether further work should continue in a new turn or after rebasing/fetching
|
||||
|
||||
## Example Triggers
|
||||
|
||||
- `Use $gframework-batch-boot and keep reducing analyzer warnings until the branch diff vs origin/main approaches 75 files.`
|
||||
- `Use $gframework-batch-boot to continue this repetitive test refactor in bounded batches until the warning count drops below 10.`
|
||||
- `Use $gframework-batch-boot and refresh module docs in waves without asking me to trigger every round.`
|
||||
4
.agents/skills/gframework-batch-boot/agents/openai.yaml
Normal file
4
.agents/skills/gframework-batch-boot/agents/openai.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "GFramework Batch Boot"
|
||||
short_description: "Run boot, then iterate bounded bulk batches"
|
||||
default_prompt: "Use $gframework-batch-boot to start from the normal GFramework boot context and continue the current repetitive task in automatic bounded batches until the declared stop condition is reached."
|
||||
File diff suppressed because it is too large
Load Diff
@ -132,462 +132,545 @@ public sealed class ContextRegistrationAnalyzerTests
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Warning_When_FieldInjectedModel_Is_Not_Registered()
|
||||
{
|
||||
var markup = MarkupTestSource.Parse(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
// Keep scenario fixtures at class scope so MA0051 reduction does not change analyzer inputs or markup spans.
|
||||
private const string MissingFieldInjectedModelRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
markup.Source,
|
||||
markup.WithSpan(
|
||||
new DiagnosticResult("GF_ContextRegistration_001", DiagnosticSeverity.Warning)
|
||||
.WithArguments("IInventoryModel", "InventoryPanelSystem", "GameArchitecture"),
|
||||
"0"));
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string RegisteredFieldInjectedModelSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string MissingHandWrittenGetSystemRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Extensions;
|
||||
|
||||
public interface ICombatSystem : ISystem { }
|
||||
|
||||
public sealed class UiUtility : IUtility
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
{|#0:this.GetSystem<ICombatSystem>()|};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterUtility(new UiUtility());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string ModuleProvidedModelRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class InventoryModule : IArchitectureModule
|
||||
{
|
||||
public void Install(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new InventoryModule());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AmbiguousOwningArchitectureSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class FirstArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SecondArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string MissingGetUtilitiesRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryUtility : IUtility { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetUtilities]
|
||||
private IReadOnlyList<IInventoryUtility> {|#0:_utilities|} = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DerivedArchitectureVirtualHelperRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public abstract class ArchitectureBase : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterComponents();
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : ArchitectureBase
|
||||
{
|
||||
protected override void RegisterComponents()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DerivedModuleVirtualHelperRegistrationSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public abstract class ModuleBase : IArchitectureModule
|
||||
{
|
||||
public void Install(IArchitecture architecture)
|
||||
{
|
||||
RegisterComponents(architecture);
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DerivedInventoryModule : ModuleBase
|
||||
{
|
||||
protected override void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new DerivedInventoryModule());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DerivedArchitectureBaseHelperCallSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public abstract class ArchitectureBase : Architecture
|
||||
{
|
||||
protected virtual void RegisterComponents()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : ArchitectureBase
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
base.RegisterComponents();
|
||||
}
|
||||
|
||||
protected override void RegisterComponents()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DerivedModuleBaseHelperCallSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public abstract class ModuleBase : IArchitectureModule
|
||||
{
|
||||
public virtual void Install(IArchitecture architecture)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DerivedInventoryModule : ModuleBase
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
base.RegisterComponents(architecture);
|
||||
}
|
||||
|
||||
protected override void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
architecture.RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new DerivedInventoryModule());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证字段注入模型未注册时会报告缺失注册告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public Task Reports_Warning_When_FieldInjectedModel_Is_Not_Registered()
|
||||
{
|
||||
return RunWarningScenarioAsync(
|
||||
MissingFieldInjectedModelRegistrationSource,
|
||||
CreateContextRegistrationWarning(
|
||||
"GF_ContextRegistration_001",
|
||||
"IInventoryModel",
|
||||
"InventoryPanelSystem",
|
||||
"GameArchitecture"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字段注入模型已注册时不会产生误报。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Does_Not_Report_When_FieldInjectedModel_Is_Registered()
|
||||
public Task Does_Not_Report_When_FieldInjectedModel_Is_Registered()
|
||||
{
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunNoDiagnosticScenarioAsync(RegisteredFieldInjectedModelSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证手写扩展方法访问未注册 System 时会报告缺失注册告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Reports_Warning_When_HandWrittenGetSystem_Call_Has_No_Registration()
|
||||
public Task Reports_Warning_When_HandWrittenGetSystem_Call_Has_No_Registration()
|
||||
{
|
||||
var markup = MarkupTestSource.Parse(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Extensions;
|
||||
|
||||
public interface ICombatSystem : ISystem { }
|
||||
|
||||
public sealed class UiUtility : IUtility
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
{|#0:this.GetSystem<ICombatSystem>()|};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterUtility(new UiUtility());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
markup.Source,
|
||||
markup.WithSpan(
|
||||
new DiagnosticResult("GF_ContextRegistration_002", DiagnosticSeverity.Warning)
|
||||
.WithArguments("ICombatSystem", "UiUtility", "GameArchitecture"),
|
||||
"0"));
|
||||
return RunWarningScenarioAsync(
|
||||
MissingHandWrittenGetSystemRegistrationSource,
|
||||
CreateContextRegistrationWarning(
|
||||
"GF_ContextRegistration_002",
|
||||
"ICombatSystem",
|
||||
"UiUtility",
|
||||
"GameArchitecture"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证模块安装链路提供注册时分析器会把该注册视为有效来源。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Does_Not_Report_When_Registration_Comes_From_Installed_Module()
|
||||
public Task Does_Not_Report_When_Registration_Comes_From_Installed_Module()
|
||||
{
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class InventoryModule : IArchitectureModule
|
||||
{
|
||||
public void Install(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new InventoryModule());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunNoDiagnosticScenarioAsync(ModuleProvidedModelRegistrationSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证无法唯一推导所属 Architecture 时分析器保持静默以避免误报。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Does_Not_Report_When_Owning_Architecture_Cannot_Be_Uniquely_Determined()
|
||||
public Task Does_Not_Report_When_Owning_Architecture_Cannot_Be_Uniquely_Determined()
|
||||
{
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class FirstArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SecondArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunNoDiagnosticScenarioAsync(AmbiguousOwningArchitectureSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证集合注入 Utility 缺失注册时仍会报告对应告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Reports_Warning_When_GetUtilities_Field_Has_No_Registered_Utility()
|
||||
public Task Reports_Warning_When_GetUtilities_Field_Has_No_Registered_Utility()
|
||||
{
|
||||
var markup = MarkupTestSource.Parse(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryUtility : IUtility { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetUtilities]
|
||||
private IReadOnlyList<IInventoryUtility> {|#0:_utilities|} = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
markup.Source,
|
||||
markup.WithSpan(
|
||||
new DiagnosticResult("GF_ContextRegistration_003", DiagnosticSeverity.Warning)
|
||||
.WithArguments("IInventoryUtility", "InventoryPanelSystem", "GameArchitecture"),
|
||||
"0"));
|
||||
return RunWarningScenarioAsync(
|
||||
MissingGetUtilitiesRegistrationSource,
|
||||
CreateContextRegistrationWarning(
|
||||
"GF_ContextRegistration_003",
|
||||
"IInventoryUtility",
|
||||
"InventoryPanelSystem",
|
||||
"GameArchitecture"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证基类初始化经由虚方法分派到派生实现时,派生注册仍会被识别。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task
|
||||
public Task
|
||||
Does_Not_Report_When_Inherited_OnInitialize_Calls_Virtual_Helper_Overridden_In_Derived_Architecture()
|
||||
{
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public abstract class ArchitectureBase : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterComponents();
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : ArchitectureBase
|
||||
{
|
||||
protected override void RegisterComponents()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunNoDiagnosticScenarioAsync(DerivedArchitectureVirtualHelperRegistrationSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证模块基类通过虚方法转发注册时,派生模块的注册依然会被识别。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Does_Not_Report_When_Inherited_Module_Install_Calls_Virtual_Helper_Overridden_In_Derived_Module()
|
||||
public Task Does_Not_Report_When_Inherited_Module_Install_Calls_Virtual_Helper_Overridden_In_Derived_Module()
|
||||
{
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public abstract class ModuleBase : IArchitectureModule
|
||||
{
|
||||
public void Install(IArchitecture architecture)
|
||||
{
|
||||
RegisterComponents(architecture);
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DerivedInventoryModule : ModuleBase
|
||||
{
|
||||
protected override void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new DerivedInventoryModule());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunNoDiagnosticScenarioAsync(DerivedModuleVirtualHelperRegistrationSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式调用基类 helper 时,分析器按基类实际执行的注册路径发出告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Reports_Warning_When_Derived_Architecture_Explicitly_Calls_Base_Helper()
|
||||
public Task Reports_Warning_When_Derived_Architecture_Explicitly_Calls_Base_Helper()
|
||||
{
|
||||
var markup = MarkupTestSource.Parse(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public abstract class ArchitectureBase : Architecture
|
||||
{
|
||||
protected virtual void RegisterComponents()
|
||||
{
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : ArchitectureBase
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
base.RegisterComponents();
|
||||
}
|
||||
|
||||
protected override void RegisterComponents()
|
||||
{
|
||||
RegisterModel(new InventoryModel());
|
||||
RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
return RunWarningScenarioAsync(
|
||||
DerivedArchitectureBaseHelperCallSource,
|
||||
CreateContextRegistrationWarning(
|
||||
"GF_ContextRegistration_001",
|
||||
"IInventoryModel",
|
||||
"InventoryPanelSystem",
|
||||
"GameArchitecture"));
|
||||
}
|
||||
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
/// <summary>
|
||||
/// 验证模块显式调用基类 helper 时,分析器按实际执行的安装路径发出告警。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public Task Reports_Warning_When_Derived_Module_Explicitly_Calls_Base_Helper()
|
||||
{
|
||||
return RunWarningScenarioAsync(
|
||||
DerivedModuleBaseHelperCallSource,
|
||||
CreateContextRegistrationWarning(
|
||||
"GF_ContextRegistration_001",
|
||||
"IInventoryModel",
|
||||
"InventoryPanelSystem",
|
||||
"GameArchitecture"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行包含诊断标记的 analyzer 场景,并把预期诊断绑定到统一的 `#0` span。
|
||||
/// </summary>
|
||||
/// <param name="source">不含公共前导代码的测试源码。</param>
|
||||
/// <param name="expectedDiagnostic">需要命中的预期诊断。</param>
|
||||
/// <returns>代表 analyzer 验证流程的异步任务。</returns>
|
||||
private static Task RunWarningScenarioAsync(string source, DiagnosticResult expectedDiagnostic)
|
||||
{
|
||||
MarkupTestSource markup = MarkupTestSource.Parse(Wrap(source));
|
||||
return AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
markup.Source,
|
||||
markup.WithSpan(
|
||||
new DiagnosticResult("GF_ContextRegistration_001", DiagnosticSeverity.Warning)
|
||||
.WithArguments("IInventoryModel", "InventoryPanelSystem", "GameArchitecture"),
|
||||
"0"));
|
||||
markup.WithSpan(expectedDiagnostic, "0"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Warning_When_Derived_Module_Explicitly_Calls_Base_Helper()
|
||||
/// <summary>
|
||||
/// 运行不应产生诊断的 analyzer 场景。
|
||||
/// </summary>
|
||||
/// <param name="source">不含公共前导代码的测试源码。</param>
|
||||
/// <returns>代表 analyzer 验证流程的异步任务。</returns>
|
||||
private static Task RunNoDiagnosticScenarioAsync(string source)
|
||||
{
|
||||
var markup = MarkupTestSource.Parse(
|
||||
Wrap("""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
public interface IInventoryModel : IModel { }
|
||||
|
||||
public sealed class InventoryModel : IInventoryModel { }
|
||||
|
||||
public sealed class InventoryPanelSystem : ISystem
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
|
||||
public abstract class ModuleBase : IArchitectureModule
|
||||
{
|
||||
public virtual void Install(IArchitecture architecture)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DerivedInventoryModule : ModuleBase
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
base.RegisterComponents(architecture);
|
||||
}
|
||||
|
||||
protected override void RegisterComponents(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new InventoryModel());
|
||||
architecture.RegisterSystem(new InventoryPanelSystem());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
InstallModule(new DerivedInventoryModule());
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
await AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(
|
||||
markup.Source,
|
||||
markup.WithSpan(
|
||||
new DiagnosticResult("GF_ContextRegistration_001", DiagnosticSeverity.Warning)
|
||||
.WithArguments("IInventoryModel", "InventoryPanelSystem", "GameArchitecture"),
|
||||
"0"));
|
||||
return AnalyzerTestDriver<ContextRegistrationAnalyzer>.RunAsync(Wrap(source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造 Context 注册分析器的统一预期诊断,以保持断言参数顺序稳定。
|
||||
/// </summary>
|
||||
/// <param name="diagnosticId">预期诊断 ID。</param>
|
||||
/// <param name="serviceType">缺失注册的服务或依赖类型。</param>
|
||||
/// <param name="ownerType">触发访问的拥有者类型。</param>
|
||||
/// <param name="architectureType">推导出的所属 Architecture 类型。</param>
|
||||
/// <returns>配置好参数的预期诊断结果。</returns>
|
||||
private static DiagnosticResult CreateContextRegistrationWarning(
|
||||
string diagnosticId,
|
||||
string serviceType,
|
||||
string ownerType,
|
||||
string architectureType)
|
||||
{
|
||||
return new DiagnosticResult(diagnosticId, DiagnosticSeverity.Warning)
|
||||
.WithArguments(serviceType, ownerType, architectureType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将公共测试前导代码与具体场景源码拼接为完整编译单元。
|
||||
/// </summary>
|
||||
/// <param name="source">具体测试场景源码。</param>
|
||||
/// <returns>包含公共前导代码的完整源码文本。</returns>
|
||||
private static string Wrap(string source)
|
||||
{
|
||||
return $"{TestPreamble}{Environment.NewLine}{Environment.NewLine}{source}";
|
||||
|
||||
@ -3,110 +3,324 @@ using GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
namespace GFramework.SourceGenerators.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <see cref="AutoRegisterModuleGenerator" /> 在模块自动注册场景下的生成契约与输出顺序。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class AutoRegisterModuleGeneratorTests
|
||||
{
|
||||
private const string AttributeOrderSource = """
|
||||
using System;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
public sealed class CombatSystem : ISystem { }
|
||||
public sealed class AudioUtility : IUtility { }
|
||||
|
||||
[AutoRegisterModule]
|
||||
[RegisterSystem(typeof(CombatSystem))]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
[RegisterUtility(typeof(AudioUtility))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AttributeOrderExpected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private const string DeterministicOrderCommonSource = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
public sealed class CombatSystem : ISystem { }
|
||||
public sealed class AudioUtility : IUtility { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DeterministicOrderPartASource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
// Padding ensures this attribute lives later in the file than the attributes in PartB.
|
||||
// The generator should still place it first because PartA sorts before PartB.
|
||||
// padding 01
|
||||
// padding 02
|
||||
// padding 03
|
||||
// padding 04
|
||||
// padding 05
|
||||
// padding 06
|
||||
// padding 07
|
||||
// padding 08
|
||||
// padding 09
|
||||
// padding 10
|
||||
[AutoRegisterModule]
|
||||
[RegisterUtility(typeof(AudioUtility))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DeterministicOrderPartBSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
[RegisterSystem(typeof(CombatSystem))]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string DeterministicOrderExpected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private const string TypeConstraintSource = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
|
||||
[AutoRegisterModule]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
public partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
||||
where TNullableRef : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string TypeConstraintExpected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
||||
where TNullableRef : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一声明上的注册特性会按照源码中的书写顺序生成安装代码。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Module_Install_Method_In_Attribute_Order()
|
||||
public Task Generates_Module_Install_Method_In_Attribute_Order()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
public sealed class CombatSystem : ISystem { }
|
||||
public sealed class AudioUtility : IUtility { }
|
||||
|
||||
[AutoRegisterModule]
|
||||
[RegisterSystem(typeof(CombatSystem))]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
[RegisterUtility(typeof(AudioUtility))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
await GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
||||
return GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
||||
AttributeOrderSource,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", AttributeOrderExpected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,140 +329,20 @@ public class AutoRegisterModuleGeneratorTests
|
||||
[Test]
|
||||
public async Task Generates_Module_Install_Method_In_Deterministic_Order_Across_Partial_Declarations()
|
||||
{
|
||||
const string commonSource = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
public sealed class CombatSystem : ISystem { }
|
||||
public sealed class AudioUtility : IUtility { }
|
||||
}
|
||||
""";
|
||||
|
||||
const string partASource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
// Padding ensures this attribute lives later in the file than the attributes in PartB.
|
||||
// The generator should still place it first because PartA sorts before PartB.
|
||||
// padding 01
|
||||
// padding 02
|
||||
// padding 03
|
||||
// padding 04
|
||||
// padding 05
|
||||
// padding 06
|
||||
// padding 07
|
||||
// padding 08
|
||||
// padding 09
|
||||
// padding 10
|
||||
[AutoRegisterModule]
|
||||
[RegisterUtility(typeof(AudioUtility))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string partBSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
[RegisterSystem(typeof(CombatSystem))]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoRegisterModuleGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources =
|
||||
{
|
||||
("Common.cs", commonSource),
|
||||
("GameplayModule.PartA.cs", partASource),
|
||||
("GameplayModule.PartB.cs", partBSource)
|
||||
("Common.cs", DeterministicOrderCommonSource),
|
||||
("GameplayModule.PartA.cs", DeterministicOrderPartASource),
|
||||
("GameplayModule.PartB.cs", DeterministicOrderPartBSource)
|
||||
},
|
||||
GeneratedSources =
|
||||
{
|
||||
(typeof(AutoRegisterModuleGenerator), "TestApp_GameplayModule.AutoRegisterModule.g.cs",
|
||||
NormalizeLineEndings(expected))
|
||||
NormalizeLineEndings(DeterministicOrderExpected))
|
||||
}
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
@ -261,102 +355,11 @@ public class AutoRegisterModuleGeneratorTests
|
||||
/// 验证生成器会保留可空引用、notnull 与 unmanaged 约束。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged()
|
||||
public Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
|
||||
[AutoRegisterModule]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
public partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
||||
where TNullableRef : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
||||
where TNullableRef : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
await GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
||||
return GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
||||
TypeConstraintSource,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", TypeConstraintExpected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -13,7 +13,7 @@ public class SchemaConfigGeneratorEnumTests
|
||||
/// 验证对象 <c>enum</c> 文档输出与快照保持一致。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_Should_Preserve_Object_Enum_Documentation()
|
||||
public Task Snapshot_Should_Preserve_Object_Enum_Documentation()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
@ -51,14 +51,14 @@ public class SchemaConfigGeneratorEnumTests
|
||||
("monster.schema.json", schema));
|
||||
|
||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||
await AssertSnapshotAsync(result, "MonsterConfig.ObjectEnum.g.txt");
|
||||
return AssertSnapshotAsync(result, "MonsterConfig.ObjectEnum.g.txt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组项 <c>enum</c> 文档回退输出与快照保持一致。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_Should_Preserve_Array_Item_Enum_Documentation_Fallback()
|
||||
public Task Snapshot_Should_Preserve_Array_Item_Enum_Documentation_Fallback()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
@ -88,14 +88,14 @@ public class SchemaConfigGeneratorEnumTests
|
||||
("monster.schema.json", schema));
|
||||
|
||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||
await AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
|
||||
return AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证对象数组项 <c>enum</c> 文档回退输出与快照保持一致。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_Should_Preserve_Array_Object_Item_Enum_Documentation_Fallback()
|
||||
public Task Snapshot_Should_Preserve_Array_Object_Item_Enum_Documentation_Fallback()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
@ -136,7 +136,7 @@ public class SchemaConfigGeneratorEnumTests
|
||||
("monster.schema.json", schema));
|
||||
|
||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||
await AssertSnapshotAsync(result, "MonsterConfig.ArrayObjectItemEnum.g.txt");
|
||||
return AssertSnapshotAsync(result, "MonsterConfig.ArrayObjectItemEnum.g.txt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -176,11 +176,11 @@ public class SchemaConfigGeneratorEnumTests
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(snapshotFolder);
|
||||
await File.WriteAllTextAsync(path, actual);
|
||||
await File.WriteAllTextAsync(path, actual).ConfigureAwait(false);
|
||||
Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}");
|
||||
}
|
||||
|
||||
var expected = await File.ReadAllTextAsync(path);
|
||||
var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false);
|
||||
Assert.That(
|
||||
Normalize(expected),
|
||||
Is.EqualTo(Normalize(actual)),
|
||||
|
||||
@ -8,171 +8,241 @@ namespace GFramework.SourceGenerators.Tests.Config;
|
||||
[TestFixture]
|
||||
public class SchemaConfigGeneratorSnapshotTests
|
||||
{
|
||||
private const string RuntimeContractsSource = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Config
|
||||
{
|
||||
public interface IConfigTable
|
||||
{
|
||||
Type KeyType { get; }
|
||||
Type ValueType { get; }
|
||||
int Count { get; }
|
||||
}
|
||||
|
||||
public interface IConfigTable<TKey, TValue> : IConfigTable
|
||||
where TKey : notnull
|
||||
{
|
||||
TValue Get(TKey key);
|
||||
bool TryGet(TKey key, out TValue? value);
|
||||
bool ContainsKey(TKey key);
|
||||
IReadOnlyCollection<TValue> All();
|
||||
}
|
||||
|
||||
public interface IConfigRegistry
|
||||
{
|
||||
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
|
||||
where TKey : notnull;
|
||||
|
||||
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
|
||||
where TKey : notnull;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Config
|
||||
{
|
||||
public sealed class YamlConfigLoader
|
||||
{
|
||||
public YamlConfigLoader RegisterTable<TKey, TValue>(
|
||||
string tableName,
|
||||
string relativePath,
|
||||
string schemaRelativePath,
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer = null)
|
||||
where TKey : notnull
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string MonsterSchema = """
|
||||
{
|
||||
"title": "Monster Config",
|
||||
"description": "Represents one monster entry generated from schema metadata.",
|
||||
"type": "object",
|
||||
"minProperties": 4,
|
||||
"maxProperties": 8,
|
||||
"required": ["id", "name", "reward", "phases"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique monster identifier."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Monster Name",
|
||||
"description": "Localized monster display name.",
|
||||
"x-gframework-index": true,
|
||||
"minLength": 3,
|
||||
"maxLength": 16,
|
||||
"pattern": "^[A-Z][a-z]+$",
|
||||
"default": "Slime",
|
||||
"enum": ["Slime", "Goblin"]
|
||||
},
|
||||
"hp": {
|
||||
"type": "integer",
|
||||
"const": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 999,
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 1000,
|
||||
"multipleOf": 5,
|
||||
"default": 10
|
||||
},
|
||||
"dropItems": {
|
||||
"description": "Referenced drop ids.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"minContains": 1,
|
||||
"maxContains": 2,
|
||||
"uniqueItems": true,
|
||||
"contains": {
|
||||
"type": "string",
|
||||
"const": "potion"
|
||||
},
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 12,
|
||||
"enum": ["potion", "slime_gel"]
|
||||
},
|
||||
"default": ["potion"],
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"description": "Reward payload.",
|
||||
"minProperties": 2,
|
||||
"maxProperties": 2,
|
||||
"required": ["gold", "currency"],
|
||||
"properties": {
|
||||
"gold": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 10
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"enum": ["coin", "gem"]
|
||||
}
|
||||
},
|
||||
"dependentRequired": {
|
||||
"currency": ["gold"]
|
||||
},
|
||||
"dependentSchemas": {
|
||||
"currency": {
|
||||
"type": "object",
|
||||
"required": ["gold"],
|
||||
"properties": {
|
||||
"gold": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["gold"],
|
||||
"properties": {
|
||||
"gold": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"if": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"const": "gem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"type": "object",
|
||||
"required": ["gold"],
|
||||
"properties": {
|
||||
"gold": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"type": "object",
|
||||
"required": ["currency"],
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"phases": {
|
||||
"type": "array",
|
||||
"description": "Encounter phases.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["wave", "monsterId"],
|
||||
"properties": {
|
||||
"wave": {
|
||||
"type": "integer"
|
||||
},
|
||||
"monsterId": {
|
||||
"type": "string",
|
||||
"description": "Monster reference id.",
|
||||
"minLength": 2,
|
||||
"maxLength": 32,
|
||||
"x-gframework-ref-table": "monster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_SchemaConfigGenerator()
|
||||
public Task Snapshot_SchemaConfigGenerator()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Config
|
||||
{
|
||||
public interface IConfigTable
|
||||
{
|
||||
Type KeyType { get; }
|
||||
Type ValueType { get; }
|
||||
int Count { get; }
|
||||
}
|
||||
|
||||
public interface IConfigTable<TKey, TValue> : IConfigTable
|
||||
where TKey : notnull
|
||||
{
|
||||
TValue Get(TKey key);
|
||||
bool TryGet(TKey key, out TValue? value);
|
||||
bool ContainsKey(TKey key);
|
||||
IReadOnlyCollection<TValue> All();
|
||||
}
|
||||
|
||||
public interface IConfigRegistry
|
||||
{
|
||||
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
|
||||
where TKey : notnull;
|
||||
|
||||
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
|
||||
where TKey : notnull;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Config
|
||||
{
|
||||
public sealed class YamlConfigLoader
|
||||
{
|
||||
public YamlConfigLoader RegisterTable<TKey, TValue>(
|
||||
string tableName,
|
||||
string relativePath,
|
||||
string schemaRelativePath,
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer = null)
|
||||
where TKey : notnull
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"title": "Monster Config",
|
||||
"description": "Represents one monster entry generated from schema metadata.",
|
||||
"type": "object",
|
||||
"minProperties": 4,
|
||||
"maxProperties": 8,
|
||||
"required": ["id", "name", "reward", "phases"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique monster identifier."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Monster Name",
|
||||
"description": "Localized monster display name.",
|
||||
"x-gframework-index": true,
|
||||
"minLength": 3,
|
||||
"maxLength": 16,
|
||||
"pattern": "^[A-Z][a-z]+$",
|
||||
"default": "Slime",
|
||||
"enum": ["Slime", "Goblin"]
|
||||
},
|
||||
"hp": {
|
||||
"type": "integer",
|
||||
"const": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 999,
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 1000,
|
||||
"multipleOf": 5,
|
||||
"default": 10
|
||||
},
|
||||
"dropItems": {
|
||||
"description": "Referenced drop ids.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"minContains": 1,
|
||||
"maxContains": 2,
|
||||
"uniqueItems": true,
|
||||
"contains": {
|
||||
"type": "string",
|
||||
"const": "potion"
|
||||
},
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 12,
|
||||
"enum": ["potion", "slime_gel"]
|
||||
},
|
||||
"default": ["potion"],
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"description": "Reward payload.",
|
||||
"minProperties": 2,
|
||||
"maxProperties": 2,
|
||||
"required": ["gold", "currency"],
|
||||
"properties": {
|
||||
"gold": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 10
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"enum": ["coin", "gem"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"phases": {
|
||||
"type": "array",
|
||||
"description": "Encounter phases.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["wave", "monsterId"],
|
||||
"properties": {
|
||||
"wave": {
|
||||
"type": "integer"
|
||||
},
|
||||
"monsterId": {
|
||||
"type": "string",
|
||||
"description": "Monster reference id.",
|
||||
"minLength": 2,
|
||||
"maxLength": 32,
|
||||
"x-gframework-ref-table": "monster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var generatedSources = GenerateSourcesForMonsterSchema();
|
||||
var snapshotFolder = GetSchemaSnapshotFolder();
|
||||
return AssertAllSnapshotsAsync(generatedSources, snapshotFolder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行 monster schema 场景,并把生成结果转换为按 hint name 索引的字典。
|
||||
/// </summary>
|
||||
/// <returns>当前快照场景的全部生成文件内容。</returns>
|
||||
private static IReadOnlyDictionary<string, string> GenerateSourcesForMonsterSchema()
|
||||
{
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
RuntimeContractsSource,
|
||||
("monster.schema.json", MonsterSchema));
|
||||
|
||||
var generatedSources = result.Results
|
||||
return result.Results
|
||||
.Single()
|
||||
.GeneratedSources
|
||||
.ToDictionary(
|
||||
static sourceResult => sourceResult.HintName,
|
||||
static sourceResult => sourceResult.SourceText.ToString(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 schema 生成器快照目录,确保断言始终落在仓库内已提交的 snapshot 资产上。
|
||||
/// </summary>
|
||||
/// <returns>schema 生成器快照目录的绝对路径。</returns>
|
||||
private static string GetSchemaSnapshotFolder()
|
||||
{
|
||||
var snapshotFolder = Path.Combine(
|
||||
TestContext.CurrentContext.TestDirectory,
|
||||
"..",
|
||||
@ -181,14 +251,7 @@ public class SchemaConfigGeneratorSnapshotTests
|
||||
"Config",
|
||||
"snapshots",
|
||||
"SchemaConfigGenerator");
|
||||
snapshotFolder = Path.GetFullPath(snapshotFolder);
|
||||
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt");
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs",
|
||||
"MonsterConfigBindings.g.txt");
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "GeneratedConfigCatalog.g.cs",
|
||||
"GeneratedConfigCatalog.g.txt");
|
||||
return Path.GetFullPath(snapshotFolder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -213,17 +276,45 @@ public class SchemaConfigGeneratorSnapshotTests
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(snapshotFolder);
|
||||
await File.WriteAllTextAsync(path, actual);
|
||||
await File.WriteAllTextAsync(path, actual).ConfigureAwait(false);
|
||||
Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}");
|
||||
}
|
||||
|
||||
var expected = await File.ReadAllTextAsync(path);
|
||||
var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false);
|
||||
Assert.That(
|
||||
Normalize(expected),
|
||||
Is.EqualTo(Normalize(actual)),
|
||||
$"Snapshot mismatch: {generatedFileName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 依次验证 schema 生成器产出的全部核心快照文件。
|
||||
/// </summary>
|
||||
/// <param name="generatedSources">生成结果字典。</param>
|
||||
/// <param name="snapshotFolder">快照目录。</param>
|
||||
/// <returns>全部快照断言完成后的异步任务。</returns>
|
||||
private static async Task AssertAllSnapshotsAsync(
|
||||
IReadOnlyDictionary<string, string> generatedSources,
|
||||
string snapshotFolder)
|
||||
{
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt")
|
||||
.ConfigureAwait(false);
|
||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt")
|
||||
.ConfigureAwait(false);
|
||||
await AssertSnapshotAsync(
|
||||
generatedSources,
|
||||
snapshotFolder,
|
||||
"MonsterConfigBindings.g.cs",
|
||||
"MonsterConfigBindings.g.txt")
|
||||
.ConfigureAwait(false);
|
||||
await AssertSnapshotAsync(
|
||||
generatedSources,
|
||||
snapshotFolder,
|
||||
"GeneratedConfigCatalog.g.cs",
|
||||
"GeneratedConfigCatalog.g.txt")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标准化快照文本以避免平台换行差异。
|
||||
/// </summary>
|
||||
|
||||
@ -78,7 +78,7 @@ public sealed partial class MonsterConfig
|
||||
/// Reward payload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Constraints: minProperties = 2, maxProperties = 2.
|
||||
/// Constraints: minProperties = 2, maxProperties = 2, dependentRequired = { currency => [gold] }, dependentSchemas = { currency => object (required = [gold]) }, allOf = [ object (required = [gold]) ], if/then/else = if object; properties = { currency: string (const = "gem") }; then object (required = [gold]); properties = { gold: integer }; else object (required = [currency]); properties = { currency: string }.
|
||||
/// </remarks>
|
||||
public sealed partial class RewardConfig
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ public static class AnalyzerTestDriver<TAnalyzer>
|
||||
/// <param name="source">测试输入源码。</param>
|
||||
/// <param name="diagnostics">期望诊断集合。</param>
|
||||
/// <returns>异步测试任务。</returns>
|
||||
public static async Task RunAsync(
|
||||
public static Task RunAsync(
|
||||
string source,
|
||||
params DiagnosticResult[] diagnostics)
|
||||
{
|
||||
@ -29,6 +29,6 @@ public static class AnalyzerTestDriver<TAnalyzer>
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.AddRange(diagnostics);
|
||||
await test.RunAsync();
|
||||
return test.RunAsync();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
@ -28,74 +29,12 @@ public static class GeneratorSnapshotTest<TGenerator>
|
||||
string snapshotFolder,
|
||||
Func<string, string>? snapshotFileNameSelector = null)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||
var compilation = CSharpCompilation.Create(
|
||||
$"{typeof(TGenerator).Name}SnapshotTests",
|
||||
[syntaxTree],
|
||||
MetadataReferenceTestBuilder.GetRuntimeMetadataReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(
|
||||
generators: [CreateGenerator()],
|
||||
parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||
driver = driver.RunGeneratorsAndUpdateCompilation(
|
||||
compilation,
|
||||
out var updatedCompilation,
|
||||
out var generatorDiagnostics);
|
||||
var (driver, updatedCompilation, generatorDiagnostics) = RunGenerator(source);
|
||||
AssertNoGeneratorErrors(generatorDiagnostics);
|
||||
AssertNoCompilationErrors(updatedCompilation);
|
||||
|
||||
var generatorErrors = generatorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
generatorErrors,
|
||||
Is.Empty,
|
||||
() =>
|
||||
$"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||
|
||||
var compilationErrors = updatedCompilation.GetDiagnostics()
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
compilationErrors,
|
||||
Is.Empty,
|
||||
() =>
|
||||
$"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||
|
||||
var runResult = driver.GetRunResult();
|
||||
var generated = runResult.Results
|
||||
.SelectMany(static result => result.GeneratedSources)
|
||||
.OrderBy(static source => source.HintName, StringComparer.Ordinal)
|
||||
.Select(static source => (filename: source.HintName, content: source.SourceText.ToString()))
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
generated,
|
||||
Is.Not.Empty,
|
||||
$"生成器 '{typeof(TGenerator).FullName}' 未产生任何输出。");
|
||||
|
||||
foreach (var (filename, content) in generated)
|
||||
{
|
||||
// 不同测试套件可能需要将生成文件映射到非 .cs 快照,以避免测试资产被当作可编译源码参与构建。
|
||||
var snapshotFileName = snapshotFileNameSelector?.Invoke(filename) ?? filename;
|
||||
var path = ResolveSnapshotPath(
|
||||
snapshotFolder,
|
||||
snapshotFileName);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
// 第一次运行:生成 snapshot
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
await File.WriteAllTextAsync(path, content.ToString());
|
||||
|
||||
Assert.Fail(
|
||||
$"未找到快照文件,已在以下路径生成新快照:\n{path}");
|
||||
}
|
||||
|
||||
var expected = await File.ReadAllTextAsync(path);
|
||||
|
||||
Assert.That(
|
||||
Normalize(expected),
|
||||
Is.EqualTo(Normalize(content.ToString())),
|
||||
$"快照不匹配:{snapshotFileName}");
|
||||
}
|
||||
var generatedSources = GetGeneratedSources(driver);
|
||||
await AssertGeneratedSnapshotsAsync(generatedSources, snapshotFolder, snapshotFileNameSelector).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -105,7 +44,163 @@ public static class GeneratorSnapshotTest<TGenerator>
|
||||
/// <returns>标准化后的文本</returns>
|
||||
private static string Normalize(string text)
|
||||
{
|
||||
return text.Replace("\r\n", "\n").Trim();
|
||||
return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建测试编译并执行目标生成器,返回更新后的编译结果和生成器诊断。
|
||||
/// </summary>
|
||||
/// <param name="source">要交给生成器处理的输入源码。</param>
|
||||
/// <returns>包含驱动、更新后编译和生成器诊断的元组。</returns>
|
||||
private static (GeneratorDriver Driver, Compilation UpdatedCompilation, ImmutableArray<Diagnostic> GeneratorDiagnostics)
|
||||
RunGenerator(string source)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||
var compilation = CreateCompilation(syntaxTree);
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(
|
||||
generators: [CreateGenerator()],
|
||||
parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||
|
||||
driver = driver.RunGeneratorsAndUpdateCompilation(
|
||||
compilation,
|
||||
out var updatedCompilation,
|
||||
out var generatorDiagnostics);
|
||||
|
||||
return (driver, updatedCompilation, generatorDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为快照测试创建最小可运行的 Roslyn 编译上下文。
|
||||
/// </summary>
|
||||
/// <param name="syntaxTree">由测试输入生成的语法树。</param>
|
||||
/// <returns>包含运行时元数据引用的动态链接库编译对象。</returns>
|
||||
private static CSharpCompilation CreateCompilation(SyntaxTree syntaxTree)
|
||||
{
|
||||
return CSharpCompilation.Create(
|
||||
$"{typeof(TGenerator).Name}SnapshotTests",
|
||||
[syntaxTree],
|
||||
MetadataReferenceTestBuilder.GetRuntimeMetadataReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言生成器自身没有报告错误级诊断。
|
||||
/// </summary>
|
||||
/// <param name="generatorDiagnostics">生成器执行期间产生的诊断集合。</param>
|
||||
private static void AssertNoGeneratorErrors(ImmutableArray<Diagnostic> generatorDiagnostics)
|
||||
{
|
||||
var generatorErrors = generatorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
generatorErrors,
|
||||
Is.Empty,
|
||||
() =>
|
||||
$"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言合并生成结果后的最终编译仍然可通过。
|
||||
/// </summary>
|
||||
/// <param name="updatedCompilation">已注入生成输出的编译对象。</param>
|
||||
private static void AssertNoCompilationErrors(Compilation updatedCompilation)
|
||||
{
|
||||
var compilationErrors = updatedCompilation.GetDiagnostics()
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
compilationErrors,
|
||||
Is.Empty,
|
||||
() =>
|
||||
$"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集并排序生成器输出,保持快照断言顺序稳定。
|
||||
/// </summary>
|
||||
/// <param name="driver">已经执行完成的生成器驱动。</param>
|
||||
/// <returns>按 HintName 排序后的生成文件名与内容。</returns>
|
||||
private static (string Filename, string Content)[] GetGeneratedSources(GeneratorDriver driver)
|
||||
{
|
||||
var generatedSources = driver.GetRunResult()
|
||||
.Results
|
||||
.SelectMany(static result => result.GeneratedSources)
|
||||
.OrderBy(static source => source.HintName, StringComparer.Ordinal)
|
||||
.Select(static source => (source.HintName, source.SourceText.ToString()))
|
||||
.ToArray();
|
||||
|
||||
Assert.That(
|
||||
generatedSources,
|
||||
Is.Not.Empty,
|
||||
$"生成器 '{typeof(TGenerator).FullName}' 未产生任何输出。");
|
||||
|
||||
return generatedSources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逐个比对生成输出与已提交快照,必要时写出缺失快照并中断测试。
|
||||
/// </summary>
|
||||
/// <param name="generatedSources">已排序的生成文件名与内容。</param>
|
||||
/// <param name="snapshotFolder">快照根目录。</param>
|
||||
/// <param name="snapshotFileNameSelector">可选的快照文件名映射规则。</param>
|
||||
/// <returns>当全部快照比对完成后结束的异步任务。</returns>
|
||||
private static async Task AssertGeneratedSnapshotsAsync(
|
||||
(string Filename, string Content)[] generatedSources,
|
||||
string snapshotFolder,
|
||||
Func<string, string>? snapshotFileNameSelector)
|
||||
{
|
||||
foreach (var (filename, content) in generatedSources)
|
||||
{
|
||||
var snapshotFileName = snapshotFileNameSelector?.Invoke(filename) ?? filename;
|
||||
var expected = await ReadExpectedSnapshotAsync(
|
||||
snapshotFolder,
|
||||
snapshotFileName,
|
||||
content)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(
|
||||
Normalize(expected),
|
||||
Is.EqualTo(Normalize(content)),
|
||||
$"快照不匹配:{snapshotFileName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定快照;若快照不存在,则先写出当前生成结果并通过断言提示调用方提交资产。
|
||||
/// </summary>
|
||||
/// <param name="snapshotFolder">快照根目录。</param>
|
||||
/// <param name="snapshotFileName">映射后的快照文件名。</param>
|
||||
/// <param name="generatedContent">当前生成器输出内容。</param>
|
||||
/// <returns>现有快照的文本内容。</returns>
|
||||
private static async Task<string> ReadExpectedSnapshotAsync(
|
||||
string snapshotFolder,
|
||||
string snapshotFileName,
|
||||
string generatedContent)
|
||||
{
|
||||
// 不同测试套件可能需要将生成文件映射到非 .cs 快照,以避免测试资产被当作可编译源码参与构建。
|
||||
var path = ResolveSnapshotPath(snapshotFolder, snapshotFileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
await WriteMissingSnapshotAndFailAsync(path, generatedContent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await File.ReadAllTextAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为首次运行缺失的快照写入当前结果,并立即终止测试以提醒提交新资产。
|
||||
/// </summary>
|
||||
/// <param name="path">目标快照绝对路径。</param>
|
||||
/// <param name="generatedContent">要写入的生成输出。</param>
|
||||
private static async Task WriteMissingSnapshotAndFailAsync(string path, string generatedContent)
|
||||
{
|
||||
// ResolveSnapshotPath 保证快照不会越界,但根目录路径仍会让 GetDirectoryName 返回 null。
|
||||
var snapshotDirectory = Path.GetDirectoryName(path)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Snapshot path '{path}' must include a parent directory.");
|
||||
Directory.CreateDirectory(snapshotDirectory);
|
||||
await File.WriteAllTextAsync(path, generatedContent).ConfigureAwait(false);
|
||||
Assert.Fail($"未找到快照文件,已在以下路径生成新快照:\n{path}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -20,8 +20,8 @@ public class GeneratorSnapshotTestSecurityTests
|
||||
var snapshotRoot = CreateSnapshotRoot();
|
||||
var source = BuildSource();
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
|
||||
Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
|
||||
source,
|
||||
snapshotRoot,
|
||||
_ => Path.Combine(snapshotRoot, "Status.EnumExtensions.g.cs")));
|
||||
@ -36,8 +36,8 @@ public class GeneratorSnapshotTestSecurityTests
|
||||
var snapshotRoot = CreateSnapshotRoot();
|
||||
var source = BuildSource();
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
|
||||
Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
|
||||
source,
|
||||
snapshotRoot,
|
||||
_ => Path.Combine("..", "escaped", "Status.EnumExtensions.g.cs")));
|
||||
|
||||
@ -13,11 +13,11 @@ public static class GeneratorTest<TGenerator>
|
||||
/// <param name="source">输入的源代码</param>
|
||||
/// <param name="generatedSources">期望生成的源文件集合,包含文件名和内容的元组</param>
|
||||
/// <returns>异步操作任务</returns>
|
||||
public static async Task RunAsync(
|
||||
public static Task RunAsync(
|
||||
string source,
|
||||
params (string filename, string content)[] generatedSources)
|
||||
{
|
||||
await RunAsync(
|
||||
return RunAsync(
|
||||
source,
|
||||
additionalReferences: [],
|
||||
generatedSources);
|
||||
@ -30,7 +30,7 @@ public static class GeneratorTest<TGenerator>
|
||||
/// <param name="additionalReferences">附加元数据引用,用于构造多程序集场景。</param>
|
||||
/// <param name="generatedSources">期望生成的源文件集合,包含文件名和内容的元组。</param>
|
||||
/// <returns>异步操作任务。</returns>
|
||||
public static async Task RunAsync(
|
||||
public static Task RunAsync(
|
||||
string source,
|
||||
IEnumerable<MetadataReference> additionalReferences,
|
||||
params (string filename, string content)[] generatedSources)
|
||||
@ -52,7 +52,7 @@ public static class GeneratorTest<TGenerator>
|
||||
foreach (var additionalReference in additionalReferences)
|
||||
test.TestState.AdditionalReferences.Add(additionalReference);
|
||||
|
||||
await test.RunAsync();
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -4,559 +4,232 @@ using GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
namespace GFramework.SourceGenerators.Tests.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <see cref="LoggerGenerator" /> 在常见日志声明配置下的快照输出保持稳定。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class LoggerGeneratorSnapshotTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证默认配置下的类日志字段快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_DefaultConfiguration_Class()
|
||||
public Task Snapshot_DefaultConfiguration_Class()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
[Log]
|
||||
public partial class MyService
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("DefaultConfiguration_Class"));
|
||||
return RunScenarioAsync(
|
||||
"DefaultConfiguration_Class",
|
||||
"[Log]",
|
||||
"public partial class MyService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证自定义 logger 名称会反映到生成快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_CustomName_Class()
|
||||
public Task Snapshot_CustomName_Class()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
[Log(Name = "CustomLogger")]
|
||||
public partial class MyService
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("CustomName_Class"));
|
||||
return RunScenarioAsync(
|
||||
"CustomName_Class",
|
||||
"[Log(Name = \"CustomLogger\")]",
|
||||
"public partial class MyService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证自定义字段名会反映到生成快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_CustomFieldName_Class()
|
||||
public Task Snapshot_CustomFieldName_Class()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
[Log(FieldName = "MyLogger")]
|
||||
public partial class MyService
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("CustomFieldName_Class"));
|
||||
return RunScenarioAsync(
|
||||
"CustomFieldName_Class",
|
||||
"[Log(FieldName = \"MyLogger\")]",
|
||||
"public partial class MyService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证实例字段模式会反映到生成快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_InstanceField_Class()
|
||||
public Task Snapshot_InstanceField_Class()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
[Log(IsStatic = false)]
|
||||
public partial class MyService
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("InstanceField_Class"));
|
||||
return RunScenarioAsync(
|
||||
"InstanceField_Class",
|
||||
"[Log(IsStatic = false)]",
|
||||
"public partial class MyService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证公共字段可见性会反映到生成快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_PublicField_Class()
|
||||
public Task Snapshot_PublicField_Class()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
[Log(AccessModifier = "public")]
|
||||
public partial class MyService
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("PublicField_Class"));
|
||||
return RunScenarioAsync(
|
||||
"PublicField_Class",
|
||||
"[Log(AccessModifier = \"public\")]",
|
||||
"public partial class MyService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证泛型类声明的日志字段快照。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Snapshot_GenericClass()
|
||||
public Task Snapshot_GenericClass()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
return RunScenarioAsync(
|
||||
"GenericClass",
|
||||
"[Log]",
|
||||
"public partial class MyService<T>");
|
||||
}
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 为给定场景组装最小测试源并执行快照校验。
|
||||
/// </summary>
|
||||
/// <param name="scenarioName">快照场景名称。</param>
|
||||
/// <param name="logAttributeLine">目标类型上的 <c>[Log(...)]</c> 声明。</param>
|
||||
/// <param name="classDeclaration">目标 partial 类型声明。</param>
|
||||
/// <returns>表示快照测试完成的异步任务。</returns>
|
||||
private static Task RunScenarioAsync(string scenarioName, string logAttributeLine, string classDeclaration)
|
||||
{
|
||||
return GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
CreateSource(logAttributeLine, classDeclaration),
|
||||
GetSnapshotFolder(scenarioName));
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 生成日志源生成器测试所需的最小宿主源代码。
|
||||
/// </summary>
|
||||
/// <param name="logAttributeLine">目标类型上的 <c>[Log(...)]</c> 声明。</param>
|
||||
/// <param name="classDeclaration">目标 partial 类型声明。</param>
|
||||
/// <returns>可直接送入快照测试的完整源码字符串。</returns>
|
||||
private static string CreateSource(string logAttributeLine, string classDeclaration)
|
||||
{
|
||||
return string.Join(
|
||||
$"{Environment.NewLine}{Environment.NewLine}",
|
||||
CreateLoggingAttributeSource(),
|
||||
CreateLoggingContractsSource(),
|
||||
CreateLoggingRuntimeSource(),
|
||||
CreateTestAppSource(logAttributeLine, classDeclaration));
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
/// <summary>
|
||||
/// 生成日志测试使用的 attribute 定义源码。
|
||||
/// </summary>
|
||||
/// <returns>包含 <c>LogAttribute</c> 的源码片段。</returns>
|
||||
private static string CreateLoggingAttributeSource()
|
||||
{
|
||||
return """
|
||||
using System;
|
||||
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Logging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class LogAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FieldName { get; set; }
|
||||
public string AccessModifier { get; set; }
|
||||
public bool IsStatic { get; set; } = true;
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 生成日志抽象契约源码,供测试编译图引用。
|
||||
/// </summary>
|
||||
/// <returns>包含 <c>ILogger</c> 的源码片段。</returns>
|
||||
private static string CreateLoggingContractsSource()
|
||||
{
|
||||
return """
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Error(string message);
|
||||
void Warn(string message);
|
||||
void Debug(string message);
|
||||
void Trace(string message);
|
||||
void Fatal(string message);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
/// <summary>
|
||||
/// 生成最小运行时宿主源码,供生成器解析 logger provider 依赖。
|
||||
/// </summary>
|
||||
/// <returns>包含 provider 与 mock logger 的源码片段。</returns>
|
||||
private static string CreateLoggingRuntimeSource()
|
||||
{
|
||||
return """
|
||||
namespace GFramework.Core.Logging
|
||||
{
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
public static class LoggerFactoryResolver
|
||||
{
|
||||
public static ILoggerProvider Provider { get; set; }
|
||||
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
public static ILoggerProvider CreateLogger(string name)
|
||||
{
|
||||
return Provider ?? new MockLoggerProvider();
|
||||
}
|
||||
}
|
||||
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
public interface ILoggerProvider
|
||||
{
|
||||
ILogger CreateLogger(string name);
|
||||
}
|
||||
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
internal class MockLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new MockLogger(name);
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
internal class MockLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
[Log]
|
||||
public partial class MyService<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
public MockLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
await GeneratorSnapshotTest<LoggerGenerator>.RunAsync(
|
||||
source,
|
||||
GetSnapshotFolder("GenericClass"));
|
||||
public void Info(string message) { }
|
||||
public void Error(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Debug(string message) { }
|
||||
public void Trace(string message) { }
|
||||
public void Fatal(string message) { }
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成实际承载 <c>[Log]</c> 声明的测试类型源码。
|
||||
/// </summary>
|
||||
/// <param name="logAttributeLine">目标类型上的 <c>[Log(...)]</c> 声明。</param>
|
||||
/// <param name="classDeclaration">目标 partial 类型声明。</param>
|
||||
/// <returns>测试应用命名空间下的目标类型源码片段。</returns>
|
||||
private static string CreateTestAppSource(string logAttributeLine, string classDeclaration)
|
||||
{
|
||||
return $$"""
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
||||
|
||||
{{logAttributeLine}}
|
||||
{{classDeclaration}}
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,8 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-028`
|
||||
- 当前阶段:`Phase 28`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-034`
|
||||
- 当前阶段:`Phase 34`
|
||||
- 当前焦点:
|
||||
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
|
||||
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
|
||||
@ -38,6 +38,9 @@
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
|
||||
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
|
||||
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
|
||||
- 已完成 `SchemaConfigGenerator.cs` 剩余 `MA0051` 收口:将 `dependentRequired` / `allOf` / conditional schema 校验
|
||||
拆成更小的验证阶段,并将 `GenerateTableClass`、`GenerateBindingsClass`、`AppendGeneratedConfigCatalogType`
|
||||
拆成稳定的代码发射 helper,保持生成输出与快照一致
|
||||
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning,
|
||||
不再默认留给长期 warning 清理分支
|
||||
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
|
||||
@ -46,12 +49,40 @@
|
||||
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `9` 条,剩余均为
|
||||
`SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `0` 条
|
||||
- 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:测试辅助器改为直接返回 `Task`,
|
||||
文件 I/O 显式补齐 `ConfigureAwait(false)`,`AnalyzerTestDriver` 文件名与类型名重新对齐
|
||||
- 当前 `GFramework.SourceGenerators.Tests` warnings-only 基线已从 `61` 条降到 `49` 条,剩余 warning 均为 `MA0051`
|
||||
- 已完成 `GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs` 的 `MA0051` 收口:同构 snapshot
|
||||
场景已收敛为模板化 helper,保留原有快照目录与生成器输入语义不变
|
||||
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `49` 条降到 `43` 条;`LoggerGeneratorSnapshotTests.cs`
|
||||
已不再出现在 `MA0051` 列表中
|
||||
- 已完成 `GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs` 的 `MA0051` 收口:
|
||||
将内联测试源码与期望快照抽到类级常量、补齐测试类 XML 文档,并将仅作转发的异步测试改为直接返回 `Task`
|
||||
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `43` 条降到 `40` 条;
|
||||
`AutoRegisterModuleGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs` 的 `MA0051` 收口:
|
||||
将 monster 场景的运行时契约与 schema 输入提取为类级常量,并把生成结果与快照目录解析拆成小 helper,保持
|
||||
生成文件名、快照目录和断言语义不变
|
||||
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `40` 条降到 `39` 条;
|
||||
`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- 已完成当前 PR #273 review follow-up 首轮核对:确认本地仍成立的问题集中在
|
||||
`SchemaConfigGenerator` helper XML 文档、`GeneratorSnapshotTest` 的 `StringComparison.Ordinal` /
|
||||
snapshot 路径空值防御、`AutoRegisterModuleGeneratorTests` 的 XML 文档位置,以及
|
||||
`SchemaConfigGeneratorSnapshotTests` 的 monster 快照覆盖缺口
|
||||
- 已将 monster 快照场景扩展到 `dependentRequired`、`dependentSchemas`、`allOf` 与 object-focused
|
||||
`if/then/else`,以便把新增 schema 约束文档纳入 snapshot 验证
|
||||
- 已完成本轮定向验证:
|
||||
`GFramework.Game.SourceGenerators` Release build 通过;
|
||||
`GFramework.SourceGenerators.Tests` 在 `-m:1 --no-restore` 下 Release build 通过;
|
||||
`SchemaConfigGeneratorSnapshotTests` 与 `AutoRegisterModuleGeneratorTests` 定向测试共 `4` 项全部通过
|
||||
- 当前验证仍受环境/基线约束:
|
||||
`GFramework.SourceGenerators.Tests` Release build 保留既有 `MA0051` warning 基线;
|
||||
NuGet vulnerability audit 在离线环境下产生 `NU1900`
|
||||
- `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
|
||||
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
|
||||
- 下一轮默认继续拆分 `GFramework.Game.SourceGenerators` 的 `MA0051` 热点,或评估跨 target 的 `MA0158`
|
||||
锁替换风险
|
||||
- 下一轮默认重新抓取 PR #273 最新 review 线程,并确认本轮 snapshot 更新后是否还存在剩余 open thread 或
|
||||
`dotnet-format` 细项
|
||||
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
|
||||
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
|
||||
|
||||
@ -84,8 +115,15 @@
|
||||
inherited-collision 快照测试
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
|
||||
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条
|
||||
`MA0051`
|
||||
- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条
|
||||
- 已完成 `GFramework.SourceGenerators.Tests` 的首轮低风险 warning 清理;当前项目已清空 `MA0004` / `MA0048`,剩余 warning
|
||||
全部收敛为 `MA0051`
|
||||
- 已完成 `LoggerGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
|
||||
`43` 条,并通过 focused snapshot tests 保持行为不变
|
||||
- 已完成 `AutoRegisterModuleGeneratorTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
|
||||
`40` 条,并通过 focused generator tests 保持输出契约不变
|
||||
- 已完成 `SchemaConfigGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests`
|
||||
Release build 基线已降到 `39` 条,并通过 focused snapshot test 保持生成输出契约不变
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
@ -126,6 +164,10 @@
|
||||
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
|
||||
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs` 的 `MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
|
||||
并用 focused schema generator tests 验证 50 个用例通过
|
||||
- `RP-032` 已完成 `AutoRegisterModuleGeneratorTests` 的 3 个 `MA0051` 收口:通过提取类级常量承载测试源码与快照,保持
|
||||
生成文件名、断言路径与源生成输出不变;`GFramework.SourceGenerators.Tests` warnings-only 基线由 `43` 降至 `40`
|
||||
- `RP-033` 已完成 `SchemaConfigGeneratorSnapshotTests` 的 `MA0051` 收口:monster schema 运行时契约与 schema 输入已提取为
|
||||
类级常量,生成结果映射与快照目录解析已拆为小 helper;`GFramework.SourceGenerators.Tests` warnings-only 基线由 `40` 降至 `39`
|
||||
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
|
||||
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
|
||||
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
|
||||
@ -141,6 +183,13 @@
|
||||
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
|
||||
的归一化字段名冲突与 `Cqrs` 对 `dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
|
||||
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
|
||||
- `RP-029` 已完成 `SchemaConfigGenerator` 剩余 `MA0051` 收口:`GFramework.Game.SourceGenerators` 独立 Release
|
||||
warnings-only build 已清零,并通过 `SchemaConfigGenerator` focused generator tests 锁定生成输出未回退
|
||||
- `RP-030` 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:`AnalyzerTestDriver` 文件名已与
|
||||
类型名一致,测试辅助器与 schema snapshot 断言路径已改为直接返回 `Task` 或显式使用 `ConfigureAwait(false)`;
|
||||
当前测试项目 warnings-only 基线从 `61` 条降到 `49` 条,剩余均为 `MA0051`
|
||||
- `RP-031` 已完成 `LoggerGeneratorSnapshotTests` 的 `MA0051` 收口:重复场景源码改为模板化 helper 生成,
|
||||
当前 `GFramework.SourceGenerators.Tests` Release build 基线从 `49` 条降到 `43` 条,并通过 focused test 验证 6 个用例全部通过
|
||||
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
|
||||
|
||||
## 当前风险
|
||||
@ -153,13 +202,12 @@
|
||||
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
||||
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
|
||||
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
|
||||
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有
|
||||
`GFramework.Game.SourceGenerators` 与测试项目 warning
|
||||
- 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
|
||||
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048`
|
||||
warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
|
||||
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
|
||||
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
|
||||
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目,并在输出中混入
|
||||
测试项目自身的结构性 warning 基线
|
||||
- 缓解措施:继续以被修改项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
|
||||
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有 `43` 条既有 `MA0051` warning;
|
||||
一旦继续进入该写集,就必须把测试项目 warning 一并纳入本轮完成条件
|
||||
- 缓解措施:已先清空低风险 `MA0004` / `MA0048`,后续继续保持“单 warning family、单测试域”的节奏推进 `MA0051`
|
||||
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
|
||||
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
|
||||
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
|
||||
@ -311,13 +359,43 @@
|
||||
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning;不属于本轮写集
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`27 Passed`,`0 Failed`
|
||||
- `RP-029` 的验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;focused test 所属测试项目已同步刷新 Linux 侧 restore 资产
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`SchemaConfigGenerator.cs` 剩余 `MA0051` 已清零
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`54 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 `GFramework.SourceGenerators.Tests` `MA0048` / `MA0051` / `MA0004` warning;不属于本轮
|
||||
`GFramework.Game.SourceGenerators` 写集
|
||||
- `RP-030` 的验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`49 Warning(s)`,`0 Error(s)`;当前项目已不再出现 `MA0004` / `MA0048`,剩余 warning 全部为 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GeneratorSnapshotTestSecurityTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~SchemaConfigGeneratorEnumTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`6 Passed`,`0 Failed`
|
||||
- `RP-031` 的验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;刷新 Linux 侧 restore 资产以支持后续串行 build/test 验证
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:Summary`
|
||||
- 结果:`43 Warning(s)`,`0 Error(s)`;`LoggerGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~LoggerGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`6 Passed`,`0 Failed`
|
||||
- `RP-033` 的验证结果:
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`39 Warning(s)`,`0 Error(s)`;`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
||||
2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`;建议先从
|
||||
`GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法切入
|
||||
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests` 的 `MA0051` 收口,先在 `GeneratorSnapshotTest`、
|
||||
`ContextRegistrationAnalyzerTests` 或 `ContextGetGeneratorTests` 中选择一个单写集推进,不再把已清零的 `MA0004` / `MA0048` 混回写集
|
||||
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
|
||||
`object` lock
|
||||
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
|
||||
@ -1,5 +1,172 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-23 — RP-033
|
||||
|
||||
### 阶段:`SchemaConfigGeneratorSnapshotTests.cs` `MA0051` 收口(RP-033)
|
||||
|
||||
- 启动复核:
|
||||
- 按 `gframework-boot` 流程恢复当前 worktree,读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、
|
||||
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
|
||||
仍映射到 `analyzer-warning-reduction`
|
||||
- 用
|
||||
`dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
复核当前 `MA0051` 热点,确认 `SchemaConfigGeneratorSnapshotTests.cs` 仍保留 1 个超长方法,适合作为单文件低风险写集
|
||||
- 决策:
|
||||
- 保持 monster schema 场景的输入源码、schema 文本、生成文件名与快照目录不变,只收敛测试方法长度
|
||||
- 沿用前几轮 snapshot test 的收口策略:提取类级常量承载大段 fixture 输入,再用小 helper 封装生成结果映射与快照目录解析
|
||||
- 同一测试项目的 build/test 继续采用串行验证;并行执行会在 WSL worktree 上制造瞬时输出缺失,导致 `MSB3030` / `CS0006`
|
||||
- 实施调整:
|
||||
- 为 `SchemaConfigGeneratorSnapshotTests` 新增 `RuntimeContractsSource` 与 `MonsterSchema` 类级常量,保留既有 monster 场景内容
|
||||
- 把生成结果字典构造拆到 `GenerateSourcesForMonsterSchema()`,把快照目录解析拆到 `GetSchemaSnapshotFolder()`
|
||||
- 保持 `AssertAllSnapshotsAsync(...)`、快照文件名与断言流程不变,不改生成器逻辑和 snapshot 资产
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`39 Warning(s)`,`0 Error(s)`;`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若继续压缩 `GFramework.SourceGenerators.Tests` 的 `MA0051`,优先处理只剩单个超长方法的 `GeneratorSnapshotTest` 或
|
||||
`ContextRegistrationAnalyzerTests`
|
||||
- 若希望继续按 warning 数量收敛,则回到 `ContextGetGeneratorTests.cs`,但需要接受更大的单文件写集
|
||||
|
||||
## 2026-04-23 — RP-032
|
||||
|
||||
### 阶段:`AutoRegisterModuleGeneratorTests.cs` `MA0051` 收口(RP-032)
|
||||
|
||||
- 启动复核:
|
||||
- 按 `gframework-boot` 流程恢复当前 worktree,读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、
|
||||
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
|
||||
仍映射到 `analyzer-warning-reduction`
|
||||
- 先用
|
||||
`dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
复核当前 `MA0051` 热点,确认 `AutoRegisterModuleGeneratorTests.cs` 仍有 `3` 个超长方法,适合作为单文件低风险写集
|
||||
- 决策:
|
||||
- 保持 `AutoRegisterModuleGeneratorTests` 的测试输入、生成文件名、快照文本与断言结构不变,只收敛方法长度
|
||||
- 采用“提取类级常量承载大段测试源码与期望输出”的方式,避免引入新的共享 helper 或改变场景组装顺序
|
||||
- 验证阶段改为串行执行 build/test;避免和同项目并行运行时拿到不完整 `bin/Release` 输出
|
||||
- 实施调整:
|
||||
- 为 `AutoRegisterModuleGeneratorTests` 补齐测试类 XML 文档
|
||||
- 将 3 个长测试方法中的源码与期望快照提取为类级 `const string`,保留原有生成文件名与断言目标
|
||||
- 将仅转发 `GeneratorTest.RunAsync(...)` 的两个异步测试改为直接返回 `Task`
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`40 Warning(s)`,`0 Error(s)`;`AutoRegisterModuleGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --disable-build-servers --filter FullyQualifiedName~AutoRegisterModuleGeneratorTests -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`3 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若继续压缩 `GFramework.SourceGenerators.Tests` 的 `MA0051`,优先处理仅剩单个超长方法的
|
||||
`GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs`
|
||||
- 若希望单次继续多降几条 warning,则改选 `ContextGetGeneratorTests.cs`,但需要接受更大的单文件写集
|
||||
|
||||
## 2026-04-23 — RP-031
|
||||
|
||||
### 阶段:`LoggerGeneratorSnapshotTests.cs` `MA0051` 收口(RP-031)
|
||||
|
||||
- 启动复核:
|
||||
- 按 `gframework-boot` 流程恢复当前 worktree,读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、
|
||||
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
|
||||
仍映射到 `analyzer-warning-reduction`
|
||||
- 结合 `RP-030` 的下一步建议与当前文件规模,优先选择 `GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs`
|
||||
作为单文件、同构 snapshot 场景的低风险写集
|
||||
- 决策:
|
||||
- 保持 `LoggerGenerator` 现有快照资产、场景命名与输入语义不变,只压缩测试方法和样板源码构造的结构复杂度
|
||||
- 先把重复场景统一为模板化 helper,再根据 analyzer 结果继续拆分 helper,直到 `LoggerGeneratorSnapshotTests.cs`
|
||||
不再出现在 `MA0051` 输出中
|
||||
- 验证阶段避免并行运行同一测试项目的 build/test,防止 WSL worktree 上的 `bin/Release` 文件占用噪音污染结果
|
||||
- 实施调整:
|
||||
- 为 `LoggerGeneratorSnapshotTests` 补齐类与测试方法 XML 文档
|
||||
- 将 6 个 snapshot 场景改为统一调用 `RunScenarioAsync(...)`
|
||||
- 将原先重复内联的完整测试源码拆成 `CreateLoggingAttributeSource()`、
|
||||
`CreateLoggingContractsSource()`、`CreateLoggingRuntimeSource()` 与 `CreateTestAppSource(...)`
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:Summary`
|
||||
- 结果:`43 Warning(s)`,`0 Error(s)`;`LoggerGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~LoggerGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`6 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若继续 `GFramework.SourceGenerators.Tests` 的 `MA0051` 治理,优先选择 `AutoRegisterModuleGeneratorTests` 或
|
||||
`GeneratorSnapshotTest` 作为下一批单写集
|
||||
- 若需要先压缩 warning 数量而不是单文件难度,可转向 `ContextGetGeneratorTests`,但应先明确本轮允许的文件数上限
|
||||
|
||||
## 2026-04-23 — RP-030
|
||||
|
||||
### 阶段:`GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口(RP-030)
|
||||
|
||||
- 启动复核:
|
||||
- 按 `gframework-boot` 流程恢复当前 worktree 后,读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、
|
||||
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
|
||||
仍映射到 `analyzer-warning-reduction`
|
||||
- 先对 `GFramework.SourceGenerators.Tests` 执行
|
||||
`dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`,
|
||||
刷新 Linux 侧 restore 资产,规避 Windows fallback package folder 干扰
|
||||
- 用
|
||||
`dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
复核当前基线,确认该测试项目共有 `61` 条 warning,其中低风险切片集中在 `MA0004` 与单个 `MA0048`
|
||||
- 决策:
|
||||
- 不直接进入大型 snapshot/test 方法的 `MA0051`,先收口纯 test-infrastructure 层的 `MA0004` / `MA0048`
|
||||
- 对“只是转发异步调用”的 helper 直接返回 `Task`,只在真实文件 I/O 上显式补 `ConfigureAwait(false)`,避免无意义的
|
||||
`async/await` 包装
|
||||
- 将 `AnalyzerTestDriver<TAnalyzer>` 所在文件改名为与类型一致,单独清理 `MA0048`,不改类型名与调用方契约
|
||||
- 实施调整:
|
||||
- 将 `AnalyzerTestDriver.RunAsync(...)` 与 `GeneratorTest.RunAsync(...)` 改为直接返回下游 `Task`
|
||||
- 为 `GeneratorSnapshotTest`、`SchemaConfigGeneratorSnapshotTests` 与 `SchemaConfigGeneratorEnumTests` 中的异步文件读写
|
||||
显式补齐 `ConfigureAwait(false)`,并把仅作转发的测试方法改为直接返回 `Task`
|
||||
- 将 `GeneratorSnapshotTestSecurityTests` 的 `Assert.ThrowsAsync(...)` 改为直接返回目标 `Task`,移除无收益的
|
||||
`async` 包装
|
||||
- 将 `GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs` 重命名为
|
||||
`GFramework.SourceGenerators.Tests/Core/AnalyzerTestDriver.cs`
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`49 Warning(s)`,`0 Error(s)`;当前项目已不再出现 `MA0004` / `MA0048`,剩余 warning 全部为 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GeneratorSnapshotTestSecurityTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~SchemaConfigGeneratorEnumTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`6 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若继续 analyzer warning reduction,继续把 `GFramework.SourceGenerators.Tests` 作为独立写集,只处理 `MA0051`
|
||||
- 下一轮优先选择单一测试域的同构长方法,例如 `LoggerGeneratorSnapshotTests`、`AutoRegisterModuleGeneratorTests`
|
||||
或共享 helper `GeneratorSnapshotTest`
|
||||
|
||||
## 2026-04-23 — RP-029
|
||||
|
||||
### 阶段:`SchemaConfigGenerator.cs` 剩余 `MA0051` 收口(RP-029)
|
||||
|
||||
- 启动复核:
|
||||
- 按 `gframework-boot` 流程恢复当前 worktree 后,先读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md`
|
||||
与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch` 仍映射到
|
||||
`analyzer-warning-reduction`
|
||||
- 用历史基线命令重新执行 `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`,
|
||||
复现 `SchemaConfigGenerator.cs` 剩余 `9` 条 `MA0051`
|
||||
- 决策:
|
||||
- 继续沿用“低风险结构拆分、不改诊断 ID、不改生成顺序、不改快照输出”的收口策略
|
||||
- 先把 schema 元数据校验方法拆成更小验证阶段,再把 `GenerateTableClass`、`GenerateBindingsClass` 与
|
||||
`AppendGeneratedConfigCatalogType` 的代码发射流程分段,避免直接改动生成文本内容
|
||||
- focused test 仍以 `SchemaConfigGenerator` 相关用例为主;`GFramework.SourceGenerators.Tests` 里既有测试项目 warning
|
||||
不纳入本轮写集
|
||||
- 实施调整:
|
||||
- 为 `dependentRequired`、`dependentSchemas`、`allOf`、conditional schema 等对象级校验补上细粒度 helper,
|
||||
把 declared-properties 获取、分支校验、target 校验拆成独立阶段
|
||||
- 为生成代码头部、表包装、bindings metadata/references、catalog metadata 发射补充结构化 helper,
|
||||
将长方法按“头部 / 元数据 / 行为方法”拆分
|
||||
- 修正 `References` 代码发射 helper 的闭合范围,确保重构后的 `MonsterConfigBindings.g.cs` 与现有快照保持一致
|
||||
- 在构建阶段遇到 Linux `dotnet` 命中 Windows fallback package folder 时,先对
|
||||
`GFramework.Game.SourceGenerators` 与 `GFramework.SourceGenerators.Tests` 执行
|
||||
`dotnet restore -p:RestoreFallbackFolders=""`,再继续 `--no-restore` 验证
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`54 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍打印既有 `MA0048` / `MA0051` / `MA0004` warning;这些 warning 属于 `GFramework.SourceGenerators.Tests`
|
||||
基线,不属于本轮 `GFramework.Game.SourceGenerators` 写集
|
||||
- 下一步建议:
|
||||
- 若继续 analyzer warning reduction,可评估是否为 `GFramework.SourceGenerators.Tests` 单独开新的 warning 清理切片
|
||||
- 若改回推进运行时主线,则按 `RP-017` 记录的策略先设计 `MA0158` 的多 target 兼容方案,再决定是否动共享 `object` lock
|
||||
|
||||
## 2026-04-23 — RP-028
|
||||
|
||||
### 阶段:`CqrsHandlerRegistryGenerator.cs` 文件级冲突化解(RP-028)
|
||||
@ -797,3 +964,20 @@
|
||||
|
||||
1. 若继续 analyzer warning reduction,优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏
|
||||
2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归
|
||||
# 2026-04-23
|
||||
|
||||
- RP-034 / PR #273 review follow-up:
|
||||
- 使用 `gframework-pr-review` 抓取当前分支 PR #273 的 latest-head review threads、MegaLinter 和测试摘要。
|
||||
- 本地复核后确认仍成立的项集中在 `SchemaConfigGenerator` helper XML 文档、
|
||||
`GeneratorSnapshotTest` 的 `StringComparison.Ordinal` 与 snapshot 路径空值防御、
|
||||
`AutoRegisterModuleGeneratorTests` 的 XML 文档位置,以及
|
||||
`SchemaConfigGeneratorSnapshotTests` 的 monster snapshot 覆盖缺口。
|
||||
- 已扩展 monster schema 场景以覆盖 `dependentRequired`、`dependentSchemas`、`allOf` 与 object-focused
|
||||
`if/then/else`,并同步更新 `MonsterConfig.g.txt` 的约束快照。
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
通过;离线 NuGet vulnerability audit 产生 `NU1900`。
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1`
|
||||
通过;测试项目保留既有 `MA0051` warning 基线。
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~AutoRegisterModuleGeneratorTests" -m:1`
|
||||
通过,`4` 个用例全部通过;需要在沙箱外执行以绕过 `vstest` 本地 socket 权限限制。
|
||||
- 下一步:提交本轮修复并在需要时重新抓取 PR review,确认 open threads 是否随新提交收敛。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user