-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCultureMutationTests.cs
More file actions
734 lines (641 loc) · 29.8 KB
/
Copy pathCultureMutationTests.cs
File metadata and controls
734 lines (641 loc) · 29.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
namespace NetEvolve.FrameShift.Tests.Integration.Analyzers;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis;
using NetEvolve.FrameShift.Analyzers;
using NetEvolve.FrameShift.Diagnostics;
using NetEvolve.FrameShift.Tests.Infrastructure;
using NetEvolve.FrameShift.TestSurface;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;
/// <summary>
/// Drives the six operators of the culture-sensitivity family through <see cref="MutationCoverageAnalyzer" />
/// end to end, so that the family is proven to produce the diagnostics a consumer sees in its build log
/// instead of merely to construct mutations.
/// </summary>
/// <remarks>
/// <para>
/// Every test states the exact set of reported gaps as one text block, built from the identifier, the
/// 1-based line and the full message of each diagnostic. That is stricter than a set of line numbers,
/// because a culture operator produces several mutants at one and the same location, and it is what makes
/// a test fail when an operator stops firing, starts firing twice or renames a mutation.
/// </para>
/// <para>
/// Each fixture pairs the member under inspection with <c>Fixture.Reached.Identity</c>, whose body carries
/// no mutation point at all. Naming that member in the manifest is what gives the analyzer a non-empty
/// reachable set — without one it reports an unusable manifest and stays silent about the code — while
/// contributing not a single diagnostic of its own.
/// </para>
/// </remarks>
public class CultureMutationTests
{
private const string ProductionAssemblyName = "ProductionAssembly";
/// <summary>
/// The member every fixture declares to make the manifest resolvable, and whose <c>return value;</c>
/// carries no mutation point.
/// </summary>
private const string AnchorMemberId = "M:Fixture.Reached.Identity(System.Int32)~System.Int32";
/// <summary>
/// The test method id every manifest of this fixture attributes its references to. No test asserts on
/// it, because these tests state what the culture operators report, not which test reached what.
/// </summary>
private const string AnonymousTestId = "M:Fixture.Tests.AnonymousTests.Reaches";
/// <summary>
/// The case count recorded for <see cref="AnonymousTestId" />: a lower bound, because nothing here
/// establishes how many input combinations the reaching test carries.
/// </summary>
private const string LowerBoundCount = "1+";
/// <summary>
/// The comparing member of <see cref="ComparisonSource" />, used to cover it instead of the anchor.
/// </summary>
private const string ComparisonMemberId = "M:Fixture.Names.AreSame(System.String,System.String)~System.Boolean";
/// <summary>
/// The text the assertions use for "not a single gap was reported".
/// </summary>
private const string NoGaps = "<no gaps>";
/// <summary>
/// The line feed the expectations are joined with, instead of <see cref="Environment.NewLine" />, so
/// that the very same text is produced on Windows and on Linux.
/// </summary>
private const string LineFeed = "\n";
private const int ComparisonLine = 17;
private const int FormatLine = 17;
private const int RemovableProviderLine = 23;
private const int RequiredProviderLine = 28;
private const int CaseConversionLine = 23;
private const int RegexOptionsLine = 17;
private const int DenseLine = 17;
/// <summary>
/// <c>Names.AreSame</c> on line 15 compares with <c>StringComparison.Ordinal</c> on line 17. Neither
/// operand is a literal, so the comparison is the only mutation point of the whole file.
/// </summary>
private const string ComparisonSource = """
namespace Fixture;
using System;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public static class Names
{
public static bool AreSame(string left, string right)
{
return string.Equals(left, right, StringComparison.Ordinal);
}
}
""";
/// <summary>
/// <c>Amount.Format</c> formats with an explicit culture on line 17, which is a mutation point of two
/// operators at once: the culture can be swapped, and the provider argument can be dropped.
/// </summary>
private const string CultureSource = """
namespace Fixture;
using System.Globalization;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public static class Amount
{
public static string Format(int value)
{
return value.ToString(CultureInfo.InvariantCulture);
}
}
""";
/// <summary>
/// The two halves of the provider removal. <c>int.ToString</c> on line 23 has a provider-less overload,
/// so dropping the provider yields a mutant that binds. <c>IRenderer.Render</c> on line 28 is declared
/// exactly once and takes the provider, so the same removal yields a mutant that does not compile.
/// </summary>
/// <remarks>
/// The provider parameter of the interface method is what makes the second call unmutatable, and the
/// interface declares no body at all, so it contributes no mutation point of its own.
/// </remarks>
private const string OverloadSource = """
namespace Fixture;
using System;
using System.Globalization;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public interface IRenderer
{
string Render(int value, IFormatProvider provider);
}
public static class Renderer
{
public static string WithProviderLessOverload(int value)
{
return value.ToString(CultureInfo.InvariantCulture);
}
public static string WithoutProviderLessOverload(IRenderer renderer, int value)
{
return renderer.Render(value, CultureInfo.InvariantCulture);
}
}
""";
/// <summary>
/// <c>Normalizer.Shout</c> converts the case of a <see cref="string" /> on line 23, while
/// <c>Normalizer.ShoutLabel</c> calls a <c>ToUpper</c> declared by <c>Label</c> on line 28. Only the
/// first one is a case conversion of <see cref="string" />, and <c>Label.ToUpper</c> returns
/// <c>string.Empty</c>, which is a field access and therefore no mutation point either.
/// </summary>
private const string CaseSource = """
namespace Fixture;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public sealed class Label
{
public string ToUpper()
{
return string.Empty;
}
}
public static class Normalizer
{
public static string Shout(string value)
{
return value.ToUpperInvariant();
}
public static string ShoutLabel(Label label)
{
return label.ToUpper();
}
}
""";
/// <summary>
/// <c>Matcher.Create</c> builds a matcher with one option flag on line 17. The pattern is a parameter
/// rather than a literal, so the flag expression is the only mutation point of the member.
/// </summary>
private const string RegexSource = """
namespace Fixture;
using System.Text.RegularExpressions;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public static class Matcher
{
public static Regex Create(string pattern)
{
return new Regex(pattern, RegexOptions.IgnoreCase);
}
}
""";
/// <summary>
/// The three positions a culture value can take that only accept a compile-time constant: an attribute
/// argument, a <see langword="const" /> initializer and a default parameter value. None of them is
/// behaviour a test could observe, so none of them is a mutation point.
/// </summary>
private const string ConstantContextSource = """
namespace Fixture;
using System;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public sealed class ComparisonAttribute : Attribute
{
public ComparisonAttribute(StringComparison comparison)
{
Comparison = comparison;
}
public StringComparison Comparison { get; }
}
public static class Annotated
{
private const StringComparison Default = StringComparison.Ordinal;
[Comparison(StringComparison.Ordinal)]
public static bool Matches(string left, string right)
{
return string.Equals(left, right, Default);
}
public static bool With(
string left,
string right,
StringComparison comparison = StringComparison.Ordinal
)
{
return string.Equals(left, right, comparison);
}
}
""";
/// <summary>
/// One member whose single expression on line 17 carries nine culture mutation points: two case
/// conversions with two mutants each and one comparison with five. It is the shape the budget has to
/// bound, because the family multiplies the mutation points of a member instead of adding one.
/// </summary>
private const string DenseSource = """
namespace Fixture;
using System;
public static class Reached
{
public static int Identity(int value)
{
return value;
}
}
public static class Dense
{
public static bool Compare(string left, string right)
{
return string.Equals(left.ToUpperInvariant(), right.ToLowerInvariant(), StringComparison.Ordinal);
}
}
""";
/// <summary>
/// The five mutants of <c>StringComparison.Ordinal</c>, in the declaration order of the enumeration and
/// each one naming the axes it moves along.
/// </summary>
private static readonly string[] _ordinalMutants =
[
"StringComparison.Ordinal => StringComparison.CurrentCulture (ordinal => culture)",
"StringComparison.Ordinal => StringComparison.CurrentCultureIgnoreCase "
+ "(ordinal => culture, case-sensitive => case-insensitive)",
"StringComparison.Ordinal => StringComparison.InvariantCulture (ordinal => culture)",
"StringComparison.Ordinal => StringComparison.InvariantCultureIgnoreCase "
+ "(ordinal => culture, case-sensitive => case-insensitive)",
"StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase (case-sensitive => case-insensitive)",
];
/// <summary>
/// Every fixture of this class, so that one test can prove that all of them compile and that none of
/// them makes the analyzer crash.
/// </summary>
/// <returns>One factory per fixture.</returns>
public static IEnumerable<Func<string>> Fixtures() =>
new[]
{
ComparisonSource,
CultureSource,
OverloadSource,
CaseSource,
RegexSource,
ConstantContextSource,
DenseSource,
}.Select(source => (Func<string>)(() => source));
/// <summary>
/// The flagship case of the family: an untested comparison reports one gap per remaining member of
/// <c>StringComparison</c>, all of them at the comparison itself.
/// </summary>
[Test]
public async Task Analyze_UntestedOrdinalComparison_ReportsAGapPerRemainingComparison()
{
var compilation = CompilationFactory.Create(ComparisonSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert.That(Gaps(diagnostics)).IsEqualTo(ExpectAt(ComparisonLine, _ordinalMutants));
_ = await Assert.That(Trivial(diagnostics)).IsEqualTo(DiagnosticAssertions.NoDiagnostics);
_ = await Assert.That(AnalyzerRunner.OfId(diagnostics, DiagnosticIds.InvalidTestSurfaceManifest)).IsEmpty();
}
}
/// <summary>
/// The same compilation and the same operator, with the comparing member itself recorded in the
/// manifest: the analysis goes completely silent, which is what makes the five gaps above a statement
/// about coverage rather than about the operator.
/// </summary>
[Test]
public async Task Analyze_CoveredOrdinalComparison_ReportsNothing()
{
var compilation = CompilationFactory.Create(ComparisonSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(ComparisonMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(DiagnosticAssertions.Describe(diagnostics))
.IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// Formatting with an explicit culture is a mutation point of two operators at once, and both of them
/// report: the culture can become the ambient one, and the provider can vanish entirely.
/// </summary>
[Test]
public async Task Analyze_UntestedInvariantCultureFormatting_ReportsTheCultureSwapAndTheProviderRemoval()
{
var compilation = CompilationFactory.Create(CultureSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(Gaps(diagnostics))
.IsEqualTo(
Expect(
(FormatLine, "InvariantCulture => CurrentCulture"),
(FormatLine, "CultureInfo.InvariantCulture => (removed)")
)
);
_ = await Assert.That(Trivial(diagnostics)).IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// The interaction with the mutant compiler the unit tests can only document: the operator offers the
/// removal for every call that passes a provider, and the analyzer reports it only where the remaining
/// arguments still bind to an overload. Line 23 has a provider-less overload and is reported, line 28
/// has none and is not, while the culture swap of the very same argument is reported on both lines —
/// so the difference cannot be explained by the operator having stayed silent.
/// </summary>
[Test]
public async Task Analyze_ProviderRemovalWithoutAMatchingOverload_IsDroppedAsANonCompilingMutant()
{
var compilation = CompilationFactory.Create(OverloadSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(Gaps(diagnostics))
.IsEqualTo(
Expect(
(RemovableProviderLine, "CultureInfo.InvariantCulture => (removed)"),
(RemovableProviderLine, "InvariantCulture => CurrentCulture"),
(RequiredProviderLine, "InvariantCulture => CurrentCulture")
)
);
_ = await Assert.That(Trivial(diagnostics)).IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// The other half of the same claim: with the verification switched off, the removal on line 28 is
/// reported as well. The mutant is therefore genuinely produced by the operator and genuinely dropped
/// by the mutant compiler, which is the only reason it is missing above.
/// </summary>
[Test]
public async Task Analyze_ProviderRemovalWithoutVerification_ReportsTheNonCompilingMutantAsWell()
{
var compilation = CompilationFactory.Create(OverloadSource, ProductionAssemblyName);
var manifest = new[] { CreateManifest(AnchorMemberId) };
var diagnostics = await RunAsync(compilation, manifest, CreateVerificationOptions(verify: false))
.ConfigureAwait(false);
_ = await Assert
.That(Gaps(diagnostics))
.IsEqualTo(
Expect(
(RemovableProviderLine, "CultureInfo.InvariantCulture => (removed)"),
(RemovableProviderLine, "InvariantCulture => CurrentCulture"),
(RequiredProviderLine, "CultureInfo.InvariantCulture => (removed)"),
(RequiredProviderLine, "InvariantCulture => CurrentCulture")
)
);
}
/// <summary>
/// A case conversion of <see cref="string" /> reports the culture counterpart and the direction
/// counterpart, while an equally named method of another type reports nothing at all — the operator
/// resolves the called method instead of matching its name.
/// </summary>
[Test]
public async Task Analyze_UntestedInvariantCaseConversion_ReportsOnlyTheConversionsOfString()
{
var compilation = CompilationFactory.Create(CaseSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
var lines = DiagnosticAssertions
.Summarise(AnalyzerRunner.OfId(diagnostics, DiagnosticIds.UnreachableMutationPoint))
.Select(summary => summary.Line);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(Gaps(diagnostics))
.IsEqualTo(
Expect(
(CaseConversionLine, "ToUpperInvariant => ToUpper"),
(CaseConversionLine, "ToUpperInvariant => ToLowerInvariant")
)
);
_ = await Assert.That(lines.Distinct()).IsEquivalentTo(new[] { CaseConversionLine });
_ = await Assert.That(Trivial(diagnostics)).IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// The option flags of a matcher are reported as one gap per offered flag: the removal of the flag that
/// is present and the addition of each of the six that are absent.
/// </summary>
[Test]
public async Task Analyze_UntestedRegexOptions_ReportsAGapPerOfferedFlag()
{
var compilation = CompilationFactory.Create(RegexSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(Gaps(diagnostics))
.IsEqualTo(
ExpectAt(
RegexOptionsLine,
[
"RegexOptions - IgnoreCase",
"RegexOptions + CultureInvariant",
"RegexOptions + Multiline",
"RegexOptions + Singleline",
"RegexOptions + ExplicitCapture",
"RegexOptions + IgnorePatternWhitespace",
"RegexOptions + RightToLeft",
]
)
);
_ = await Assert.That(Trivial(diagnostics)).IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// A culture value in a position that only accepts a compile-time constant is not behaviour a test
/// could ever observe, so the analysis stays completely silent — not even an informational diagnostic
/// is produced, because no mutation is created in the first place.
/// </summary>
[Test]
public async Task Analyze_CultureValueInAConstantContext_ReportsNothing()
{
var compilation = CompilationFactory.Create(ConstantContextSource, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(DiagnosticAssertions.Describe(diagnostics))
.IsEqualTo(DiagnosticAssertions.NoDiagnostics);
}
}
/// <summary>
/// The budget bounds a member of the culture family exactly as it bounds any other member. One
/// expression carrying nine culture mutation points reports all nine without a budget, four of them
/// with a budget of four and one with a budget of one, always in the order the mutation points are
/// walked: the leftmost case conversion first, the comparison last.
/// </summary>
[Test]
public async Task Analyze_MemberDenseWithCultureMutations_IsBoundedByTheMutantBudget()
{
var compilation = CompilationFactory.Create(DenseSource, ProductionAssemblyName);
var manifest = new[] { CreateManifest(AnchorMemberId) };
var caseConversions = new[]
{
"ToUpperInvariant => ToUpper",
"ToUpperInvariant => ToLowerInvariant",
"ToLowerInvariant => ToLower",
"ToLowerInvariant => ToUpperInvariant",
};
var unlimited = await RunAsync(compilation, manifest).ConfigureAwait(false);
var four = await RunAsync(compilation, manifest, CreateBudget(4)).ConfigureAwait(false);
var one = await RunAsync(compilation, manifest, CreateBudget(1)).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(Errors(compilation)).IsEmpty();
_ = await Assert
.That(Gaps(unlimited))
.IsEqualTo(ExpectAt(DenseLine, [.. caseConversions, .. _ordinalMutants]));
_ = await Assert.That(Gaps(four)).IsEqualTo(ExpectAt(DenseLine, caseConversions));
_ = await Assert.That(Gaps(one)).IsEqualTo(ExpectAt(DenseLine, [caseConversions[0]]));
}
}
/// <summary>
/// Every fixture of this class compiles and is analysed without the analyzer throwing. Roslyn turns an
/// analyzer exception into <c>AD0001</c> and carries on, so a crash would otherwise look like a
/// diagnostic the tests above simply did not expect.
/// </summary>
/// <param name="source">The fixture to analyse.</param>
/// <returns>A task that completes when the fixture was analysed.</returns>
[Test]
[MethodDataSource(nameof(Fixtures))]
public async Task Analyze_EveryFixture_CompilesAndReportsNoAnalyzerFailure(string source)
{
var compilation = CompilationFactory.Create(source, ProductionAssemblyName);
var diagnostics = await RunAsync(compilation, [CreateManifest(AnchorMemberId)]).ConfigureAwait(false);
using (Assert.Multiple())
{
_ = await Assert.That(string.Join("; ", Errors(compilation))).IsEqualTo(string.Empty);
_ = await Assert.That(AnalyzerRunner.OfId(diagnostics, AnalyzerRunner.AnalyzerFailureId)).IsEmpty();
_ = await Assert.That(AnalyzerRunner.OfId(diagnostics, DiagnosticIds.InvalidTestSurfaceManifest)).IsEmpty();
}
}
private static Task<ImmutableArray<Diagnostic>> RunAsync(
Compilation compilation,
IEnumerable<AdditionalText>? additionalFiles = null,
IReadOnlyDictionary<string, string>? globalOptions = null
) => AnalyzerRunner.RunAsync(new MutationCoverageAnalyzer(), compilation, additionalFiles, globalOptions);
/// <summary>
/// Builds a manifest recording <paramref name="referencedMemberIds" /> as the production members the
/// tests of the first pass touched.
/// </summary>
/// <remarks>
/// Every reference is attributed to one anonymous test whose case count is the lower bound
/// <see cref="LowerBoundCount" />. These tests are about which mutation points are reachable and state
/// nothing about test data, so a lower bound is the honest count — and it keeps <c>FSH0006</c> silent,
/// which is what lets every exact diagnostic set below stay a statement about the culture operators
/// alone.
/// </remarks>
/// <param name="referencedMemberIds">The declaration ids of the covered members.</param>
/// <returns>The manifest as an additional file.</returns>
private static InMemoryAdditionalText CreateManifest(params string[] referencedMemberIds)
{
var builder = new StringBuilder();
_ = builder.Append(TestSurfaceManifestFormat.Header).Append('\n');
_ = builder
.Append(TestSurfaceManifestFormat.TestPrefix)
.Append(TestSurfaceManifestFormat.FieldSeparator)
.Append(AnonymousTestId)
.Append(TestSurfaceManifestFormat.FieldSeparator)
.Append(LowerBoundCount)
.Append('\n');
foreach (var referencedMemberId in referencedMemberIds)
{
_ = builder
.Append(TestSurfaceManifestFormat.ReferencePrefix)
.Append(TestSurfaceManifestFormat.FieldSeparator)
.Append(referencedMemberId)
.Append('\n');
}
return new InMemoryAdditionalText(builder.ToString());
}
private static Dictionary<string, string> CreateVerificationOptions(bool verify) =>
new Dictionary<string, string>(StringComparer.Ordinal)
{
["build_property.FrameShiftVerifyMutantCompilation"] = verify ? "true" : "false",
};
private static Dictionary<string, string> CreateBudget(int maximum) =>
new Dictionary<string, string>(StringComparer.Ordinal)
{
["build_property.FrameShiftMaxMutantsPerMember"] = ToText(maximum),
};
/// <summary>
/// Describes the reported gaps as one text block, one line per diagnostic, ordered ordinally so that
/// the result does not depend on the order the concurrently running analyzer callbacks reported them
/// in. Several gaps share one location, which is exactly the case a positional order cannot separate.
/// </summary>
/// <param name="diagnostics">All diagnostics of a run.</param>
/// <returns>The described gaps, or <see cref="NoGaps" /> when there is none.</returns>
private static string Gaps(ImmutableArray<Diagnostic> diagnostics)
{
var gaps = AnalyzerRunner.OfId(diagnostics, DiagnosticIds.UnreachableMutationPoint);
if (gaps.IsEmpty)
{
return NoGaps;
}
return Join(
DiagnosticAssertions.Summarise(gaps).Select(summary => Entry(summary.Id, summary.Line, summary.Message))
);
}
/// <summary>
/// Builds the expectation of a set of gaps that all sit on <paramref name="line" />.
/// </summary>
/// <param name="line">The 1-based line every gap is reported on.</param>
/// <param name="displayNames">The display names of the expected mutations.</param>
/// <returns>The expected text block.</returns>
private static string ExpectAt(int line, IEnumerable<string> displayNames) =>
Expect([.. displayNames.Select(displayName => (Line: line, DisplayName: displayName))]);
/// <summary>
/// Builds the expectation of a set of gaps, each one a line and the display name of its mutation.
/// </summary>
/// <param name="gaps">The expected gaps.</param>
/// <returns>The expected text block, or <see cref="NoGaps" /> when nothing is expected.</returns>
private static string Expect(params (int Line, string DisplayName)[] gaps) =>
gaps.Length == 0 ? NoGaps : Join(gaps.Select(gap => GapEntry(gap.Line, gap.DisplayName)));
/// <summary>
/// Builds the described gap of one mutation, spelling out the message
/// <see cref="Descriptors.UnreachableMutationPoint" /> formats.
/// </summary>
/// <param name="line">The 1-based line the gap is reported on.</param>
/// <param name="displayName">The display name of the mutation.</param>
/// <returns>The described gap.</returns>
private static string GapEntry(int line, string displayName) =>
Entry(
DiagnosticIds.UnreachableMutationPoint,
line,
"Mutation '"
+ displayName
+ "' at this location is not reachable from any test; a surviving mutant here would go unnoticed"
);
private static string Entry(string id, int line, string message) => $"{id} line {ToText(line)}: {message}";
private static string Join(IEnumerable<string> entries) =>
string.Join(LineFeed, entries.OrderBy(entry => entry, StringComparer.Ordinal));
private static string Trivial(ImmutableArray<Diagnostic> diagnostics) =>
DiagnosticAssertions.Describe(AnalyzerRunner.OfId(diagnostics, DiagnosticIds.TrivialMutant));
private static string ToText(int value) => value.ToString(CultureInfo.InvariantCulture);
private static ImmutableArray<Diagnostic> Errors(Compilation compilation) =>
CompilationFactory.GetCompileErrors(compilation);
}