@phuc1403/musketeer 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +1 -0
- package/README.md +1 -1
- package/manifest.json +19 -2
- package/package.json +1 -1
- package/template/.claude/hooks/validate-cml-hook.js +6 -5
- package/template/.claude/skills/logical-components/.gitattributes +2 -0
- package/template/.claude/skills/logical-components/SKILL.md +102 -0
- package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +53 -0
- package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +67 -0
- package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +39 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Build.props +7 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +8 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +69 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +39 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +70 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +39 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +65 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +187 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +166 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +87 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +83 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +196 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +111 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +72 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +79 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +15 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +73 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +203 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +155 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +75 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +27 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +76 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +132 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +173 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +84 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +178 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +59 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +53 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +75 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +63 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +227 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +41 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +82 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +34 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +42 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +176 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +190 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +174 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +17 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +72 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +32 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +23 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +50 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +65 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +119 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +162 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +34 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +49 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +227 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +27 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +87 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +63 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +28 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +16 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +78 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +33 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +63 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +17 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +9 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +19 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +107 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +143 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +12 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +89 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +16 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +35 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +13 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +13 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/global.json +5 -0
- package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +26 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
using System.Text;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents.Tests;
|
|
4
|
+
|
|
5
|
+
[Collection(FixtureSolutions.Name)]
|
|
6
|
+
public class ComponentDiscoveryTests(SampleAppWorkspace sample)
|
|
7
|
+
{
|
|
8
|
+
private const string OutOfRepoWarning =
|
|
9
|
+
"declaration of Sample.Billing.Application.SharedClock outside the repo was ignored";
|
|
10
|
+
|
|
11
|
+
private static readonly ExtractConfig FixtureConfig = ExtractConfig.Load(FixturePaths.SampleApp);
|
|
12
|
+
|
|
13
|
+
private Task<DiscoveryResult> DiscoverAsync(ExtractConfig? config = null) =>
|
|
14
|
+
ComponentDiscovery.DiscoverAsync(sample.Loaded, config ?? FixtureConfig, TestContext.Current.CancellationToken);
|
|
15
|
+
|
|
16
|
+
[Fact]
|
|
17
|
+
public async Task Candidates_are_the_classes_and_records_with_methods_of_their_own_in_application_and_infrastructure()
|
|
18
|
+
{
|
|
19
|
+
var result = await ComponentDiscovery.CandidatesAsync(sample.Loaded, TestContext.Current.CancellationToken);
|
|
20
|
+
|
|
21
|
+
// The fixture config classifies every candidate, so the candidates are exactly its entries. Out: records with no
|
|
22
|
+
// method (OrderRequest, Shape, Circle, QueueEntry, Job) or only a ToString override (Money), OrderSnapshot
|
|
23
|
+
// (properties and a constructor), OrderOptions (*Options), OrderRejectedException (exception), the composition
|
|
24
|
+
// roots, FileHelper (file-local), interfaces and abstract classes (seams), Receipts.Builder (nested), SharedClock
|
|
25
|
+
// (declared outside the repo). In: records with methods, and Infrastructure classes whether reached or not.
|
|
26
|
+
var types = result.Candidates.Select(c => c.Type).ToList();
|
|
27
|
+
Assert.Equal(FixtureConfig.Components.Concat(FixtureConfig.Helpers.Keys).Order(StringComparer.Ordinal), types);
|
|
28
|
+
Assert.Contains("Sample.Application.PaymentMethod", types);
|
|
29
|
+
Assert.Contains("Sample.Application.Quantity", types);
|
|
30
|
+
Assert.Contains("Sample.Infrastructure.UnreachedPlumbing", types);
|
|
31
|
+
Assert.DoesNotContain(types, t => t is "Sample.Application.Money" or "Sample.Application.OrderSnapshot"
|
|
32
|
+
or "Sample.Application.OrderRequest" or "Sample.Application.Job" or "Sample.Application.Shape"
|
|
33
|
+
or "Sample.Application.OrderOptions" or "Sample.Application.StageBase" or "Sample.Infrastructure.QueueEntry"
|
|
34
|
+
or "Sample.Application.Hooks" or "Sample.Application.Registry" or "Sample.Application.Tracked");
|
|
35
|
+
Assert.Equal([OutOfRepoWarning], result.Warnings);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[Fact]
|
|
39
|
+
public async Task Components_are_exactly_the_candidates_the_config_classifies_as_components()
|
|
40
|
+
{
|
|
41
|
+
var result = await DiscoverAsync();
|
|
42
|
+
|
|
43
|
+
Assert.Equal(FixtureConfig.Components.Order(StringComparer.Ordinal), result.Components.Select(c => c.Type));
|
|
44
|
+
Assert.DoesNotContain(result.Components, c => FixtureConfig.Helpers.ContainsKey(c.Type));
|
|
45
|
+
Assert.Empty(result.Unclassified);
|
|
46
|
+
Assert.Equal([OutOfRepoWarning], result.Warnings);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[Fact]
|
|
50
|
+
public async Task Candidates_in_neither_list_are_unclassified_and_never_components()
|
|
51
|
+
{
|
|
52
|
+
var config = FixtureConfig with
|
|
53
|
+
{
|
|
54
|
+
Components = FixtureConfig.Components.Where(c => c != "Sample.Infrastructure.Mailer").ToList(),
|
|
55
|
+
Helpers = FixtureConfig.Helpers.Where(h => h.Key != "Sample.Application.Quantity").ToDictionary(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var result = await DiscoverAsync(config);
|
|
59
|
+
|
|
60
|
+
Assert.Equal(["Sample.Application.Quantity", "Sample.Infrastructure.Mailer"], result.Unclassified);
|
|
61
|
+
Assert.DoesNotContain(result.Components, c => c.Type == "Sample.Infrastructure.Mailer");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[Fact]
|
|
65
|
+
public async Task Config_entries_naming_no_candidate_are_warnings_not_failures()
|
|
66
|
+
{
|
|
67
|
+
var config = FixtureConfig with
|
|
68
|
+
{
|
|
69
|
+
Components = [.. FixtureConfig.Components, "Sample.Application.NoSuchType", "Sample.Application.OrderRequest"],
|
|
70
|
+
Helpers = new Dictionary<string, string>(FixtureConfig.Helpers) { ["RunClock"] = "simple names do not match" },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
var result = await DiscoverAsync(config);
|
|
74
|
+
|
|
75
|
+
Assert.Equal(
|
|
76
|
+
[
|
|
77
|
+
OutOfRepoWarning,
|
|
78
|
+
"config entry 'RunClock' names no candidate; ignored",
|
|
79
|
+
"config entry 'Sample.Application.NoSuchType' names no candidate; ignored",
|
|
80
|
+
"config entry 'Sample.Application.OrderRequest' names no candidate; ignored",
|
|
81
|
+
],
|
|
82
|
+
result.Warnings);
|
|
83
|
+
Assert.Equal(FixtureConfig.Components.Count, result.Components.Count);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[Fact]
|
|
87
|
+
public async Task Clashing_simple_names_get_module_ids_and_names_while_others_stay_plain()
|
|
88
|
+
{
|
|
89
|
+
var result = await DiscoverAsync();
|
|
90
|
+
var byType = result.Components.ToDictionary(c => c.Type);
|
|
91
|
+
|
|
92
|
+
var sampleRunning = byType["Sample.Application.OrderRunning"];
|
|
93
|
+
Assert.Equal(("Sample_OrderRunning", "Order Running (Sample)", "Sample"),
|
|
94
|
+
(sampleRunning.Id, sampleRunning.Name, sampleRunning.Module));
|
|
95
|
+
|
|
96
|
+
var billingRunning = byType["Sample.Billing.Application.OrderRunning"];
|
|
97
|
+
Assert.Equal(("Billing_OrderRunning", "Order Running (Billing)", "Billing"),
|
|
98
|
+
(billingRunning.Id, billingRunning.Name, billingRunning.Module));
|
|
99
|
+
|
|
100
|
+
var acceptance = byType["Sample.Application.OrderAcceptance"];
|
|
101
|
+
Assert.Equal(("OrderAcceptance", "Order Acceptance"), (acceptance.Id, acceptance.Name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[Fact]
|
|
105
|
+
public async Task Same_module_clashes_fall_back_to_the_full_type_name_without_generic_arity_in_the_display_name()
|
|
106
|
+
{
|
|
107
|
+
var result = await DiscoverAsync();
|
|
108
|
+
var byType = result.Components.ToDictionary(c => c.Type);
|
|
109
|
+
|
|
110
|
+
Assert.Equal(("Sample_Application_Cache", "Cache (Sample.Application.Cache)"),
|
|
111
|
+
(byType["Sample.Application.Cache"].Id, byType["Sample.Application.Cache"].Name));
|
|
112
|
+
Assert.Equal(("Sample_Application_Cache_1", "Cache (Sample.Application.Cache<>)"),
|
|
113
|
+
(byType["Sample.Application.Cache`1"].Id, byType["Sample.Application.Cache`1"].Name));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[Fact]
|
|
117
|
+
public async Task Spans_are_repo_relative_class_declaration_lines_from_real_source_files_only()
|
|
118
|
+
{
|
|
119
|
+
var result = await DiscoverAsync();
|
|
120
|
+
var byId = result.Components.ToDictionary(c => c.Id);
|
|
121
|
+
|
|
122
|
+
Assert.Equal([new SourceSpan("src/Sample.Application/OrderAcceptance.cs", 4, 17)], byId["OrderAcceptance"].Spans);
|
|
123
|
+
Assert.Equal(
|
|
124
|
+
[
|
|
125
|
+
new SourceSpan("src/Sample.Application/ReceiptPrinting.Totals.cs", 3, 6),
|
|
126
|
+
new SourceSpan("src/Sample.Application/ReceiptPrinting.cs", 4, 7),
|
|
127
|
+
],
|
|
128
|
+
byId["ReceiptPrinting"].Spans);
|
|
129
|
+
// The [GeneratedRegex] half lives in generator output under obj/; it must not be a span.
|
|
130
|
+
Assert.Equal([new SourceSpan("src/Sample.Application/EdgeCases.cs", 13, 19)], byId["OrderNumberFormat"].Spans);
|
|
131
|
+
Assert.All(result.Components.SelectMany(c => c.Spans), s => Assert.StartsWith("src/", s.File));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[Fact]
|
|
135
|
+
public async Task Serialized_output_is_identical_across_runs_lf_only_bom_free_and_repo_relative()
|
|
136
|
+
{
|
|
137
|
+
var first = Serialize(await DiscoverAsync());
|
|
138
|
+
var second = Serialize(await DiscoverAsync());
|
|
139
|
+
|
|
140
|
+
Assert.Equal(first, second);
|
|
141
|
+
var text = Encoding.UTF8.GetString(first);
|
|
142
|
+
Assert.False(first.AsSpan().StartsWith(Encoding.UTF8.Preamble));
|
|
143
|
+
Assert.DoesNotContain("\r", text);
|
|
144
|
+
Assert.EndsWith("\n", text);
|
|
145
|
+
Assert.DoesNotContain(sample.Loaded.RepoRoot.Replace('\\', '/'), text, StringComparison.OrdinalIgnoreCase);
|
|
146
|
+
Assert.DoesNotContain(sample.Loaded.RepoRoot, text, StringComparison.OrdinalIgnoreCase);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[Fact]
|
|
150
|
+
public async Task Output_is_identical_for_the_same_code_checked_out_in_another_folder()
|
|
151
|
+
{
|
|
152
|
+
var copyRoot = Directory.CreateTempSubdirectory("lc-checkout-");
|
|
153
|
+
try
|
|
154
|
+
{
|
|
155
|
+
CopySource(FixturePaths.SampleApp, Path.Combine(copyRoot.FullName, "SampleApp"));
|
|
156
|
+
CopySource(Path.Combine(FixturePaths.Root, "SharedOutsideRepo"), Path.Combine(copyRoot.FullName, "SharedOutsideRepo"));
|
|
157
|
+
using var copy = await WorkspaceLoader.LoadAsync(Path.Combine(copyRoot.FullName, "SampleApp"));
|
|
158
|
+
|
|
159
|
+
var fromCopy = await ComponentDiscovery.DiscoverAsync(copy, FixtureConfig, TestContext.Current.CancellationToken);
|
|
160
|
+
|
|
161
|
+
Assert.Equal(
|
|
162
|
+
Encoding.UTF8.GetString(Serialize(await DiscoverAsync())),
|
|
163
|
+
Encoding.UTF8.GetString(Serialize(fromCopy)));
|
|
164
|
+
}
|
|
165
|
+
finally
|
|
166
|
+
{
|
|
167
|
+
copyRoot.Delete(recursive: true);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static byte[] Serialize(DiscoveryResult result) =>
|
|
172
|
+
ComponentsFile.Serialize(new ExtractResult(result.Components, [], [], result.Warnings));
|
|
173
|
+
|
|
174
|
+
// Copies source files only: bin/obj hold absolute paths of the original checkout.
|
|
175
|
+
private static void CopySource(string from, string to)
|
|
176
|
+
{
|
|
177
|
+
foreach (var file in Directory.EnumerateFiles(from, "*", SearchOption.AllDirectories))
|
|
178
|
+
{
|
|
179
|
+
var relative = Path.GetRelativePath(from, file);
|
|
180
|
+
if (relative.Split(Path.DirectorySeparatorChar).Any(s => s is "bin" or "obj"))
|
|
181
|
+
{
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var target = Path.Combine(to, relative);
|
|
186
|
+
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
|
187
|
+
File.Copy(file, target);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
[Collection(FixtureSolutions.Name)]
|
|
4
|
+
public class CouplingExtractionTests(SampleAppWorkspace sample)
|
|
5
|
+
{
|
|
6
|
+
private const string AuditTrailPortWarning = "port Sample.Application.IAuditTrail has no implementation";
|
|
7
|
+
|
|
8
|
+
private static readonly ExtractConfig FixtureConfig = ExtractConfig.Load(FixturePaths.SampleApp);
|
|
9
|
+
|
|
10
|
+
private Task<ExtractResult> ExtractAsync(ExtractConfig? config = null) =>
|
|
11
|
+
CouplingResolver.ExtractAsync(sample.Loaded, config ?? FixtureConfig, TestContext.Current.CancellationToken);
|
|
12
|
+
|
|
13
|
+
private static string Describe(Edge e) =>
|
|
14
|
+
$"{e.From} -> {e.To} [{string.Join(",", e.Kinds)}]" + (e.Via.Count > 0 ? $" via {string.Join(",", e.Via)}" : "");
|
|
15
|
+
|
|
16
|
+
[Fact]
|
|
17
|
+
public async Task Edges_are_every_way_a_component_runs_or_holds_another_seen_through_helpers_and_seams()
|
|
18
|
+
{
|
|
19
|
+
var result = await ExtractAsync();
|
|
20
|
+
|
|
21
|
+
// Not edges: anything from DependencyInjection (composition root), references to data (OrderRequest,
|
|
22
|
+
// QueueEntry), BCL/framework types (DateTime, Console, Enumerable, IReadOnlyList<string>, Lazy, Func), self calls
|
|
23
|
+
// (OrderNumberFormat.Digits, ShippingStage -> its base's Stamp), a call on a port nothing implements
|
|
24
|
+
// (IAuditTrail.Record), FakeOrderQueue (test project, never loaded), other constructions of a generic port
|
|
25
|
+
// (TextHandler), a static member on an interface (FallbackPricing -> DefaultPricing), a const read
|
|
26
|
+
// (LineCounting -> Limits), the top-level namesake of a nested type (Builder), a record property that runs
|
|
27
|
+
// nothing (JobCounting -> Job.Id), compiler-written record members (JobShowing -> Job.ToString/Equals), a
|
|
28
|
+
// property's setter when only its getter runs (TrackedReading), other constructions of a generic seam reached
|
|
29
|
+
// through a generic helper (PingSending -> PongHandling), and helpers themselves (LabelFormat, Dispatch, ...).
|
|
30
|
+
Assert.Equal(
|
|
31
|
+
[
|
|
32
|
+
"Archiving -> DiskStorage [call,di] via StorageBase",
|
|
33
|
+
"BatchRunning -> PriceFormatting [static]",
|
|
34
|
+
"BatchRunning -> RunClock [call,di,new]",
|
|
35
|
+
"CrashSettling -> ExportRecovery [call] via IRecovery",
|
|
36
|
+
"CrashSettling -> ImportRecovery [call] via IRecovery",
|
|
37
|
+
"CrashSettling -> RecoveryRouting [call,di]",
|
|
38
|
+
"Echoing -> PriceFormatting [static] via PingText",
|
|
39
|
+
"ForwardedNotifier -> Mailer [call,di]",
|
|
40
|
+
"JobReporting -> CallbackNormalizing [static] via Job",
|
|
41
|
+
"LabelMaking -> RunClock [call,new] via LabelBase,ShortLabel",
|
|
42
|
+
"Labelling -> RunClock [call] via LabelFormat",
|
|
43
|
+
"LazyWork -> ForwardedNotifier [di] via INotifier",
|
|
44
|
+
"LazyWork -> OrderQueue [di] via IOrderQueue",
|
|
45
|
+
"LazyWork -> OrderValidation [di]",
|
|
46
|
+
"LazyWork -> RunClock [di]",
|
|
47
|
+
"LoudNotifying -> Notifying [inherits]",
|
|
48
|
+
"Measuring -> PriceRules [static] via Metres",
|
|
49
|
+
"NotifyAll -> ForwardedNotifier [call,di] via INotifier",
|
|
50
|
+
"NotifyAll -> OrderTextExtensions [static]",
|
|
51
|
+
"OrderAcceptance -> ForwardedNotifier [call,di] via INotifier",
|
|
52
|
+
"OrderAcceptance -> OrderQueue [call,di] via IOrderQueue",
|
|
53
|
+
"OrderAcceptance -> OrderValidation [call,di]",
|
|
54
|
+
"OrderHandling -> LoggingHandler [call,di] via IHandler",
|
|
55
|
+
"OrderHandling -> OrderRequestHandler [call,di] via IHandler",
|
|
56
|
+
"OrderQueue -> QueueStore [call,new]",
|
|
57
|
+
"OrderValidation -> PriceRules [static]",
|
|
58
|
+
"PingSending -> PingHandling [call] via Dispatch,ICommandHandler",
|
|
59
|
+
"PongViaSeam -> PongHandling [call,di] via ICommandHandler",
|
|
60
|
+
"PongViaSeam -> RunClock [call] via ICommandHandler,TwoWay",
|
|
61
|
+
"ReceiptIssuing -> Receipts [call,new]",
|
|
62
|
+
"RecoveryRouting -> ExportRecovery [di] via IRecovery",
|
|
63
|
+
"RecoveryRouting -> ImportRecovery [di] via IRecovery",
|
|
64
|
+
"RegistryReading -> RunClock [call] via Registry",
|
|
65
|
+
"Sample_OrderRunning -> IAuditTrail [di]",
|
|
66
|
+
"Sample_OrderRunning -> RunClock [call,new]",
|
|
67
|
+
"ShippingStage -> PriceRules [static]",
|
|
68
|
+
"Stamping -> PriceRules [static] via StampedText",
|
|
69
|
+
"TickReading -> PriceRules [static] via Ticks",
|
|
70
|
+
"TitleReading -> DwgDrawing [call] via IDrawing",
|
|
71
|
+
"TrackedWriting -> CallbackNormalizing [static] via Tracked",
|
|
72
|
+
"UrgentNotifying -> Notifying [inherits]",
|
|
73
|
+
],
|
|
74
|
+
result.Edges.Select(Describe));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[Fact]
|
|
78
|
+
public async Task An_edge_seen_through_a_helper_cites_the_line_in_the_component_where_the_chain_starts()
|
|
79
|
+
{
|
|
80
|
+
var result = await ExtractAsync();
|
|
81
|
+
var at = result.Edges.ToDictionary(e => (e.From, e.To), e => e.At);
|
|
82
|
+
|
|
83
|
+
Assert.Equal("src/Sample.Application/SeeThroughCases.cs:43", at[("JobReporting", "CallbackNormalizing")]);
|
|
84
|
+
Assert.Equal("src/Sample.Application/SeeThroughCases.cs:59", at[("Labelling", "RunClock")]);
|
|
85
|
+
Assert.Equal("src/Sample.Application/SeeThroughCases.cs:72", at[("Stamping", "PriceRules")]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[Fact]
|
|
89
|
+
public async Task Each_component_reaches_exactly_the_helper_members_its_code_runs()
|
|
90
|
+
{
|
|
91
|
+
var result = await ExtractAsync();
|
|
92
|
+
var reaches = result.Components.ToDictionary(c => c.Id, c => c.Reaches ?? []);
|
|
93
|
+
const string file = "src/Sample.Application/SeeThroughCases.cs";
|
|
94
|
+
|
|
95
|
+
// The property of the record it reads, not the record's other members.
|
|
96
|
+
Assert.Equal([new SourceSpan(file, 33, 33)], reaches["JobReporting"]);
|
|
97
|
+
Assert.Equal([new SourceSpan(file, 54, 54)], reaches["Labelling"]);
|
|
98
|
+
// Constructing a helper runs its field initializers.
|
|
99
|
+
Assert.Equal([new SourceSpan(file, 65, 65)], reaches["Stamping"]);
|
|
100
|
+
Assert.Empty(reaches["PriceRules"]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[Fact]
|
|
104
|
+
public async Task Reaches_hold_only_code_that_runs_for_the_use()
|
|
105
|
+
{
|
|
106
|
+
var result = await ExtractAsync();
|
|
107
|
+
var reaches = result.Components.ToDictionary(c => c.Id, c => c.Reaches ?? []);
|
|
108
|
+
const string file = "src/Sample.Application/ReviewFixCases.cs";
|
|
109
|
+
|
|
110
|
+
// The Pong handler of a helper implementing two constructions, not the Ping one listed first.
|
|
111
|
+
Assert.Equal([new SourceSpan(file, 36, 39)], reaches["PongViaSeam"]);
|
|
112
|
+
// Compiler-written record members and a getter-only read reach nothing.
|
|
113
|
+
Assert.Empty(reaches["JobShowing"]);
|
|
114
|
+
Assert.Single(reaches["TrackedReading"]);
|
|
115
|
+
Assert.DoesNotContain(reaches["TrackedReading"], s => s.Start == 63);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[Fact]
|
|
119
|
+
public async Task An_inherits_edge_cites_the_base_list_of_the_partial_that_declares_it()
|
|
120
|
+
{
|
|
121
|
+
var result = await ExtractAsync();
|
|
122
|
+
|
|
123
|
+
Assert.Equal(
|
|
124
|
+
"src/Sample.Application/LoudNotifying.Base.cs:3",
|
|
125
|
+
result.Edges.Single(e => (e.From, e.To) == ("LoudNotifying", "Notifying")).At);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[Fact]
|
|
129
|
+
public async Task Every_classified_component_is_a_node_whether_anything_reaches_it_or_not()
|
|
130
|
+
{
|
|
131
|
+
var result = await ExtractAsync();
|
|
132
|
+
|
|
133
|
+
Assert.Equal(FixtureConfig.Components.Order(StringComparer.Ordinal), result.Components.Select(c => c.Type));
|
|
134
|
+
Assert.Contains(result.Components, c => c.Type == "Sample.Infrastructure.UnreachedPlumbing");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
[Fact]
|
|
138
|
+
public async Task A_port_with_no_implementation_is_an_external_node_with_a_warning()
|
|
139
|
+
{
|
|
140
|
+
var result = await ExtractAsync();
|
|
141
|
+
|
|
142
|
+
Assert.Equal([new ExternalNode("IAuditTrail", "Sample.Application.IAuditTrail", "I Audit Trail", "port")], result.ExternalNodes);
|
|
143
|
+
Assert.Contains(AuditTrailPortWarning, result.Warnings);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[Fact]
|
|
147
|
+
public async Task Each_edge_cites_its_first_occurrence()
|
|
148
|
+
{
|
|
149
|
+
var result = await ExtractAsync();
|
|
150
|
+
var at = result.Edges.ToDictionary(e => (e.From, e.To), e => e.At);
|
|
151
|
+
|
|
152
|
+
Assert.Equal("src/Sample.Application/OrderAcceptance.cs:4", at[("OrderAcceptance", "OrderQueue")]);
|
|
153
|
+
Assert.Equal("src/Sample.Application/OrderRunning.cs:15", at[("Sample_OrderRunning", "RunClock")]);
|
|
154
|
+
Assert.Equal("src/Sample.Application/CouplingCases.cs:5", at[("BatchRunning", "RunClock")]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[Fact]
|
|
158
|
+
public async Task Serialized_extraction_is_identical_across_runs()
|
|
159
|
+
{
|
|
160
|
+
Assert.Equal(ComponentsFile.Serialize(await ExtractAsync()), ComponentsFile.Serialize(await ExtractAsync()));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
[Fact]
|
|
164
|
+
public async Task A_repo_that_does_not_compile_is_extracted_with_a_warning_per_broken_project()
|
|
165
|
+
{
|
|
166
|
+
using var broken = await WorkspaceLoader.LoadAsync(FixturePaths.BrokenApp);
|
|
167
|
+
|
|
168
|
+
var config = ExtractConfig.Empty with { Components = ["Broken.Application.Broken"] };
|
|
169
|
+
var result = await CouplingResolver.ExtractAsync(broken, config, TestContext.Current.CancellationToken);
|
|
170
|
+
|
|
171
|
+
Assert.Equal(["project Broken.Application has 1 compile error(s); coupling may be incomplete"], result.Warnings);
|
|
172
|
+
Assert.Equal(["Broken.Application.Broken"], result.Components.Select(c => c.Type));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class DisplayNameTests
|
|
4
|
+
{
|
|
5
|
+
[Theory]
|
|
6
|
+
[InlineData("ExportJobAcceptance", "Export Job Acceptance")]
|
|
7
|
+
[InlineData("OgcServer", "Ogc Server")]
|
|
8
|
+
[InlineData("IOStream", "IO Stream")]
|
|
9
|
+
[InlineData("GeoServer", "Geo Server")]
|
|
10
|
+
[InlineData("Utf8Decoding", "Utf8 Decoding")]
|
|
11
|
+
[InlineData("HTTPClientFactory", "HTTP Client Factory")]
|
|
12
|
+
[InlineData("Run", "Run")]
|
|
13
|
+
public void Pascal_case_splits_into_words_keeping_acronyms_together(string typeName, string expected)
|
|
14
|
+
{
|
|
15
|
+
Assert.Equal(expected, DisplayName.FromTypeName(typeName));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class ExtractConfigTests
|
|
4
|
+
{
|
|
5
|
+
private static ExtractConfig LoadWith(string json)
|
|
6
|
+
{
|
|
7
|
+
var repo = Directory.CreateTempSubdirectory("lc-config-");
|
|
8
|
+
try
|
|
9
|
+
{
|
|
10
|
+
File.WriteAllText(Path.Combine(repo.FullName, ExtractConfig.FileName), json);
|
|
11
|
+
return ExtractConfig.Load(repo.FullName);
|
|
12
|
+
}
|
|
13
|
+
finally
|
|
14
|
+
{
|
|
15
|
+
repo.Delete(recursive: true);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[Fact]
|
|
20
|
+
public void A_repo_without_a_config_file_gets_the_empty_config()
|
|
21
|
+
{
|
|
22
|
+
var repo = Directory.CreateTempSubdirectory("lc-no-config-");
|
|
23
|
+
try
|
|
24
|
+
{
|
|
25
|
+
Assert.Equal(ExtractConfig.Empty, ExtractConfig.Load(repo.FullName));
|
|
26
|
+
}
|
|
27
|
+
finally
|
|
28
|
+
{
|
|
29
|
+
repo.Delete(recursive: true);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[Fact]
|
|
34
|
+
public void The_config_file_lists_components_and_helpers_with_their_reasons()
|
|
35
|
+
{
|
|
36
|
+
var config = LoadWith("""
|
|
37
|
+
{
|
|
38
|
+
"components": ["Some.App.OrderAcceptance"],
|
|
39
|
+
"helpers": { "Some.App.OrderNumber": "Value object." }
|
|
40
|
+
}
|
|
41
|
+
""");
|
|
42
|
+
|
|
43
|
+
Assert.Equal(["Some.App.OrderAcceptance"], config.Components);
|
|
44
|
+
Assert.Equal(new Dictionary<string, string> { ["Some.App.OrderNumber"] = "Value object." }, config.Helpers);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[Fact]
|
|
48
|
+
public void The_old_exclude_and_include_keys_fail_and_point_to_the_new_ones()
|
|
49
|
+
{
|
|
50
|
+
var error = Assert.Throws<ExtractConfigException>(() => LoadWith("""{ "exclude": ["HebrewAlphabet"] }"""));
|
|
51
|
+
|
|
52
|
+
Assert.Contains("'exclude'/'include' were replaced by 'components' and 'helpers'", error.Message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[Fact]
|
|
56
|
+
public void A_type_in_both_lists_fails()
|
|
57
|
+
{
|
|
58
|
+
var error = Assert.Throws<ExtractConfigException>(() => LoadWith("""
|
|
59
|
+
{ "components": ["Some.App.Clock"], "helpers": { "Some.App.Clock": "?" } }
|
|
60
|
+
"""));
|
|
61
|
+
|
|
62
|
+
Assert.Contains("'Some.App.Clock' is in both components and helpers", error.Message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Fact]
|
|
66
|
+
public void A_malformed_config_file_fails_naming_the_file()
|
|
67
|
+
{
|
|
68
|
+
var error = Assert.Throws<ExtractConfigException>(() => LoadWith("{ not json"));
|
|
69
|
+
|
|
70
|
+
Assert.Contains(ExtractConfig.FileName, error.Message);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Locates the fixture solutions under scripts/fixtures by walking up from the test binaries,
|
|
5
|
+
/// so the tests run the same from an IDE, `dotnet test`, or any working directory.
|
|
6
|
+
/// </summary>
|
|
7
|
+
internal static class FixturePaths
|
|
8
|
+
{
|
|
9
|
+
public static string Root { get; } = FindFixturesRoot();
|
|
10
|
+
|
|
11
|
+
public static string SampleApp => Path.Combine(Root, "SampleApp");
|
|
12
|
+
|
|
13
|
+
public static string NoSolutionApp => Path.Combine(Root, "NoSolutionApp");
|
|
14
|
+
|
|
15
|
+
public static string MultiTargetApp => Path.Combine(Root, "MultiTargetApp");
|
|
16
|
+
|
|
17
|
+
public static string BrokenApp => Path.Combine(Root, "BrokenApp");
|
|
18
|
+
|
|
19
|
+
private static string FindFixturesRoot()
|
|
20
|
+
{
|
|
21
|
+
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
|
|
22
|
+
{
|
|
23
|
+
var candidate = Path.Combine(dir.FullName, "fixtures");
|
|
24
|
+
if (Directory.Exists(Path.Combine(candidate, "SampleApp")))
|
|
25
|
+
{
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new DirectoryNotFoundException($"No fixtures/SampleApp found above {AppContext.BaseDirectory}");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<OutputType>Exe</OutputType>
|
|
6
|
+
<Nullable>enable</Nullable>
|
|
7
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
8
|
+
<IsPackable>false</IsPackable>
|
|
9
|
+
</PropertyGroup>
|
|
10
|
+
|
|
11
|
+
<ItemGroup>
|
|
12
|
+
<PackageReference Include="xunit.v3" Version="4.0.1" />
|
|
13
|
+
</ItemGroup>
|
|
14
|
+
|
|
15
|
+
<ItemGroup>
|
|
16
|
+
<Using Include="Xunit" />
|
|
17
|
+
</ItemGroup>
|
|
18
|
+
|
|
19
|
+
<ItemGroup>
|
|
20
|
+
<ProjectReference Include="../LogicalComponents/LogicalComponents.csproj" />
|
|
21
|
+
</ItemGroup>
|
|
22
|
+
|
|
23
|
+
</Project>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class MarkdownRendererTests
|
|
4
|
+
{
|
|
5
|
+
[Fact]
|
|
6
|
+
public void Renders_canonical_citations()
|
|
7
|
+
{
|
|
8
|
+
var responsibilities = RenderTestData.ValidResponsibilities();
|
|
9
|
+
responsibilities["OrderQueue"] = [new Responsibility("Stores each queued order.", "src/Infra/OrderQueue.cs:0010-010")];
|
|
10
|
+
|
|
11
|
+
var markdown = MarkdownRenderer.Render(RenderTestData.Extraction, responsibilities);
|
|
12
|
+
|
|
13
|
+
Assert.Contains("- Stores each queued order. (`src/Infra/OrderQueue.cs:10`)\n", markdown);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[Fact]
|
|
17
|
+
public void Diagram_ids_are_prefixed_and_sanitized_so_mermaid_keywords_and_odd_characters_cannot_break_it()
|
|
18
|
+
{
|
|
19
|
+
var extraction = RenderTestData.Extraction with
|
|
20
|
+
{
|
|
21
|
+
Components =
|
|
22
|
+
[
|
|
23
|
+
RenderTestData.Extraction.Components[0] with { Id = "end", Name = "Say \"Hi\"" },
|
|
24
|
+
RenderTestData.Extraction.Components[2] with { Id = "My Module_Queue" },
|
|
25
|
+
],
|
|
26
|
+
ExternalNodes = [],
|
|
27
|
+
Edges = [new Edge("end", "My Module_Queue", ["di"], [], "src/App/OrderAcceptance.cs:4")],
|
|
28
|
+
};
|
|
29
|
+
var responsibilities = RenderTestData.ValidResponsibilities();
|
|
30
|
+
responsibilities["end"] = responsibilities["OrderAcceptance"];
|
|
31
|
+
responsibilities["My Module_Queue"] = responsibilities["OrderQueue"];
|
|
32
|
+
|
|
33
|
+
var markdown = MarkdownRenderer.Render(extraction, responsibilities);
|
|
34
|
+
|
|
35
|
+
Assert.Contains(" n_end[\"Say #quot;Hi#quot;\"]\n", markdown);
|
|
36
|
+
Assert.Contains(" n_My_Module_Queue[\"Order Queue\"]\n", markdown);
|
|
37
|
+
Assert.Contains(" n_end --> n_My_Module_Queue\n", markdown);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[Fact]
|
|
41
|
+
public void Diagram_arrow_points_from_the_dependent_component_to_the_one_it_depends_on_without_labels()
|
|
42
|
+
{
|
|
43
|
+
var markdown = MarkdownRenderer.Render(RenderTestData.Extraction, RenderTestData.ValidResponsibilities());
|
|
44
|
+
|
|
45
|
+
Assert.Contains(" n_OrderAcceptance --> n_OrderQueue\n", markdown);
|
|
46
|
+
Assert.Contains(" n_ReceiptPrinting --> n_OrderAcceptance\n", markdown);
|
|
47
|
+
Assert.DoesNotContain("-->|", markdown);
|
|
48
|
+
Assert.DoesNotContain("classDef", markdown);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class ProjectFiltersTests
|
|
4
|
+
{
|
|
5
|
+
private const string PlainLibrary = """<Project Sdk="Microsoft.NET.Sdk"></Project>""";
|
|
6
|
+
|
|
7
|
+
[Theory]
|
|
8
|
+
[InlineData("tests/Foo/Foo.csproj")]
|
|
9
|
+
[InlineData("test/Foo/Foo.csproj")]
|
|
10
|
+
[InlineData("src/Foo.Tests/Foo.Tests.csproj")]
|
|
11
|
+
[InlineData("src/Foo.UnitTests/Foo.UnitTests.csproj")]
|
|
12
|
+
[InlineData("src/Foo.IntegrationTests/Foo.IntegrationTests.csproj")]
|
|
13
|
+
[InlineData("src/Foo.Test/Foo.Test.csproj")]
|
|
14
|
+
[InlineData("src/Foo.Specs/Foo.Specs.csproj")]
|
|
15
|
+
public void Test_folders_and_test_project_names_are_test_projects(string relativePath)
|
|
16
|
+
{
|
|
17
|
+
Assert.True(ProjectFilters.IsTestProject(relativePath, PlainLibrary));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[Theory]
|
|
21
|
+
[InlineData("""<Project Sdk="MSTest.Sdk/3.8.0"></Project>""")]
|
|
22
|
+
[InlineData("""<Project><PropertyGroup><IsTestProject> true </IsTestProject></PropertyGroup></Project>""")]
|
|
23
|
+
[InlineData("""<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />""")]
|
|
24
|
+
[InlineData("""<PackageReference Include="xunit" Version="2.9.0" />""")]
|
|
25
|
+
[InlineData("""<PackageReference Include="xunit.v3" Version="3.0.0" />""")]
|
|
26
|
+
[InlineData("""<PackageReference Include="xunit.core" Version="2.9.0" />""")]
|
|
27
|
+
[InlineData("""<PackageReference Include="NUnit" Version="4.0.0" />""")]
|
|
28
|
+
[InlineData("""<PackageReference Include="MSTest.TestFramework" Version="3.0.0" />""")]
|
|
29
|
+
[InlineData("""<PackageReference Include="Microsoft.Testing.Platform" Version="1.0.0" />""")]
|
|
30
|
+
public void Test_framework_markers_in_the_project_file_make_a_test_project(string projectText)
|
|
31
|
+
{
|
|
32
|
+
Assert.True(ProjectFilters.IsTestProject("src/Foo/Foo.csproj", projectText));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[Theory]
|
|
36
|
+
[InlineData("src/Foo.Application/Foo.Application.csproj")]
|
|
37
|
+
[InlineData("src/Foo.Contest/Foo.Contest.csproj")]
|
|
38
|
+
[InlineData("src/Latest.Feeds/Latest.Feeds.csproj")]
|
|
39
|
+
[InlineData("src/Foo.Testing/Foo.Testing.csproj")]
|
|
40
|
+
public void Source_projects_are_not_test_projects(string relativePath)
|
|
41
|
+
{
|
|
42
|
+
Assert.False(ProjectFilters.IsTestProject(relativePath, PlainLibrary));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[Theory]
|
|
46
|
+
[InlineData("Multi.Application(net10.0)", "Multi.Application(net8.0)")]
|
|
47
|
+
[InlineData("A(net8.0)", "A(netstandard2.1)")]
|
|
48
|
+
[InlineData("A(netstandard2.1)", "A(netstandard2.0)")]
|
|
49
|
+
[InlineData("A(netcoreapp3.1)", "A(netstandard2.1)")]
|
|
50
|
+
[InlineData("A(net10.0)", "A(net9.0-windows)")]
|
|
51
|
+
[InlineData("A(net472)", "A(net45)")]
|
|
52
|
+
[InlineData("A(netstandard2.0)", "A(net48)")]
|
|
53
|
+
public void Higher_target_framework_ranks_above_lower(string higher, string lower)
|
|
54
|
+
{
|
|
55
|
+
Assert.True(ProjectFilters.TargetFrameworkRank(higher).CompareTo(ProjectFilters.TargetFrameworkRank(lower)) > 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[Theory]
|
|
59
|
+
[InlineData("Multi.Application(net10.0)", "Multi.Application")]
|
|
60
|
+
[InlineData("Plain.Project", "Plain.Project")]
|
|
61
|
+
public void Target_framework_suffix_is_stripped_from_project_names(string loadedName, string expected)
|
|
62
|
+
{
|
|
63
|
+
Assert.Equal(expected, ProjectFilters.StripTargetFramework(loadedName));
|
|
64
|
+
}
|
|
65
|
+
}
|