• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoopereira / dotnet-test-rerun / 19924677100

04 Dec 2025 09:49AM UTC coverage: 96.178%. First build
19924677100

Pull #288

github

web-flow
Merge 85943984b into 0accaf9aa
Pull Request #288: v4 alpha release

160 of 178 branches covered (89.89%)

Branch coverage included in aggregate %.

135 of 146 new or added lines in 6 files covered. (92.47%)

620 of 633 relevant lines covered (97.95%)

352.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.18
/src/RerunCommand/RerunCommandConfiguration.cs
1
using System.CommandLine;
2
using System.CommandLine.Parsing;
3
using System.Text;
4
using dotnet.test.rerun.Enums;
5
using dotnet.test.rerun.Logging;
6

7
namespace dotnet.test.rerun.RerunCommand;
8

9
public class RerunCommandConfiguration
10
{
11
    #region Properties
12

13
    public string? Path { get; internal set; }
246✔
14
    public string? Filter { get; internal set; }
342✔
15
    public string? Settings { get; internal set; }
163✔
16
    public IEnumerable<string> Logger { get; internal set; } = [];
258✔
17
    public string ResultsDirectory { get; internal set; } = string.Empty;
224✔
18
    public int RerunMaxAttempts { get; internal set; }
211✔
19
    public int RerunMaxFailedTests { get; internal set; }
131✔
20
    public LogLevel LogLevel { get; internal set; }
85✔
21
    public bool NoBuild { get; internal set; }
161✔
22
    public bool NoRestore { get; internal set; }
161✔
23
    public int Delay { get; internal set; }
143✔
24
    public bool Blame { get; internal set; }
160✔
25
    public bool DeleteReportFiles { get; internal set; }
116✔
26
    public string? Collector { get; internal set; }
159✔
27
    public CoverageFormat? MergeCoverageFormat { get; internal set; }
142✔
28
    public string? Configuration { get; internal set; }
161✔
29
    public LoggerVerbosity? Verbosity { get; internal set; }
160✔
30
    public string? Framework { get; internal set; }
160✔
31
    public string ExtraArguments { get; internal set; } = string.Empty;
345✔
32
    public string InlineRunSettings { get; internal set; } = string.Empty;
250✔
33
    public IEnumerable<string>? EnvironmentVariables { get; internal set; }
159✔
34
    
35
    #endregion Properties
36

37
    #region Arguments
38

39
    private Argument<string> PathArgument = new("path")
91✔
40
    {
91✔
41
        Description = "Path to a test project .dll file.",
91✔
42
        Arity = ArgumentArity.ZeroOrOne
91✔
43
    };
91✔
44

45
    #endregion Arguments
46

47
    #region Options
48

49
    private readonly Option<string> FilterOption = new("--filter")
91✔
50
    {
91✔
51
        Description = "Run tests that match the given expression."
91✔
52
    };
91✔
53

54
    private readonly Option<string> SettingsOption = new("--settings", "-s")
91✔
55
    {
91✔
56
        Description = "The run settings file to use when running tests."
91✔
57
    };
91✔
58

59
    private readonly Option<IEnumerable<string>> LoggerOption = new("--logger", "-l")
91✔
60
    {
91✔
61
        Description = "Specifies a logger for test results.",
91✔
62
        DefaultValueFactory = _ => new[] { "trx" }
53✔
63
    };
91✔
64

65
    private readonly Option<string> ResultsDirectoryOption = new("--results-directory", "-r")
91✔
66
    {
91✔
67
        Description = "The directory where the test results will be placed.\nThe specified directory will be created if it does not exist.",
91✔
68
        DefaultValueFactory = _ => "."
20✔
69
    };
91✔
70

71
    private readonly Option<int> RerunMaxAttemptsOption = new("--rerunMaxAttempts")
91✔
72
    {
91✔
73
        Description = "Maximum # of attempts.",
91✔
74
        DefaultValueFactory = _ => 3
26✔
75
    };
91✔
76

77
    private readonly Option<int> RerunMaxFailedTestsOption = new("--rerunMaxFailedTests")
91✔
78
    {
91✔
79
        Description = "Maximum # of failed tests to rerun. If exceeded, tests will not be rerun.",
91✔
80
        DefaultValueFactory = _ => -1
69✔
81
    };
91✔
82

83
    private readonly Option<LogLevel> LogLevelOption = new("--loglevel")
91✔
84
    {
91✔
85
        Description = "Log Level",
91✔
86
        DefaultValueFactory = _ => LogLevel.Verbose,
47✔
87
        CustomParser = Logging.Logger.ParseLogLevel
91✔
88
    };
91✔
89

90
    private readonly Option<bool> NoBuildOption = new("--no-build")
91✔
91
    {
91✔
92
        Description = "Do not build the project before testing. Implies --no-restore.",
91✔
93
        Arity = ArgumentArity.Zero
91✔
94
    };
91✔
95

96
    private readonly Option<bool> NoRestoreOption = new("--no-restore")
91✔
97
    {
91✔
98
        Description = "Do not restore the project before building.",
91✔
99
        Arity = ArgumentArity.Zero
91✔
100
    };
91✔
101

102
    private readonly Option<int> DelayOption = new("--delay", "-d")
91✔
103
    {
91✔
104
        Description = "Delay between test runs in seconds."
91✔
105
    };
91✔
106

107
    private readonly Option<bool> BlameOption = new("--blame")
91✔
108
    {
91✔
109
        Description = "Runs the tests in blame mode.",
91✔
110
        Arity = ArgumentArity.Zero
91✔
111
    };
91✔
112

113
    private readonly Option<bool> DeleteReportFilesOption = new("--deleteReports")
91✔
114
    {
91✔
115
        Description = "Delete the report files generated.",
91✔
116
        Arity = ArgumentArity.Zero
91✔
117
    };
91✔
118
    
119
    private readonly Option<string> CollectorOption = new("--collect")
91✔
120
    {
91✔
121
        Description = "Enables data collector for the test run."
91✔
122
    };
91✔
123

124
    private readonly Option<CoverageFormat?> MergeCoverageFormatOption = new("--mergeCoverageFormat")
91✔
125
    {
91✔
126
        Description = "Output coverage format. Note: requires dotnet coverage tool to be installed."
91✔
127
    };
91✔
128
    
129
    private readonly Option<string> ConfigurationOption = new("--configuration", "-c")
91✔
130
    {
91✔
131
        Description = "Defines the build configuration."
91✔
132
    };
91✔
133
    
134
    private readonly Option<string> FrameworkOption = new("--framework", "-f")
91✔
135
    {
91✔
136
        Description = "Defines the target framework to run the tests."
91✔
137
    };
91✔
138
    
139
    private readonly Option<LoggerVerbosity?> VerbosityOption = new("--verbosity", "-v")
91✔
140
    {
91✔
141
        Description = "Sets the verbosity level of the command. Possible values: Quiet, Minimal, Normal, Detailed, Diagnostic"
91✔
142
    };
91✔
143
    
144
    private readonly Option<string[]> InlineRunSettingsOption = new("--inlineRunSettings")
91✔
145
    {
91✔
146
        Description = "Specifies the inline run settings.",
91✔
147
        AllowMultipleArgumentsPerToken = true
91✔
148
    };
91✔
149
    
150
    private readonly Option<IEnumerable<string>> EnvironmentVariablesOption = new("--environment", "-e")
91✔
151
    {
91✔
152
        Description = "Sets the value of an environment variable.",
91✔
153
        AllowMultipleArgumentsPerToken = true
91✔
154
    };
91✔
155

156
    #endregion Options
157

158
    private string? OriginalFilter;
159
    
160
    public void Set(Command cmd)
161
    {
162
        cmd.Arguments.Add(PathArgument);
118✔
163
        cmd.Options.Add(FilterOption);
118✔
164
        cmd.Options.Add(SettingsOption);
118✔
165
        cmd.Options.Add(LoggerOption);
118✔
166
        cmd.Options.Add(ResultsDirectoryOption);
118✔
167
        cmd.Options.Add(RerunMaxAttemptsOption);
118✔
168
        cmd.Options.Add(RerunMaxFailedTestsOption);
118✔
169
        cmd.Options.Add(LogLevelOption);
118✔
170
        cmd.Options.Add(NoBuildOption);
118✔
171
        cmd.Options.Add(NoRestoreOption);
118✔
172
        cmd.Options.Add(DelayOption);
118✔
173
        cmd.Options.Add(BlameOption);
118✔
174
        cmd.Options.Add(ConfigurationOption);
118✔
175
        cmd.Options.Add(FrameworkOption);
118✔
176
        cmd.Options.Add(VerbosityOption);
118✔
177
        cmd.Options.Add(DeleteReportFilesOption);
118✔
178
        cmd.Options.Add(CollectorOption);
118✔
179
        cmd.Options.Add(MergeCoverageFormatOption);
118✔
180
        cmd.Options.Add(InlineRunSettingsOption);
118✔
181
        cmd.Options.Add(EnvironmentVariablesOption);
118✔
182
    }
118✔
183

184
    public void GetValues(ParseResult parseResult)
185
    {
186
        Path = parseResult.GetValue(PathArgument);
72✔
187
        Filter = parseResult.GetValue(FilterOption);
72✔
188
        Settings = parseResult.GetValue(SettingsOption);
72✔
189
        Logger = parseResult.GetValue(LoggerOption)!;
72✔
190
        ResultsDirectory = parseResult.GetValue(ResultsDirectoryOption)!;
72✔
191
        RerunMaxAttempts = parseResult.GetValue(RerunMaxAttemptsOption);
72✔
192
        RerunMaxFailedTests = parseResult.GetValue(RerunMaxFailedTestsOption);
72✔
193
        LogLevel = parseResult.GetValue(LogLevelOption);
72✔
194
        NoBuild = parseResult.GetValue(NoBuildOption);
72✔
195
        NoRestore = parseResult.GetValue(NoRestoreOption);
72✔
196
        Delay = parseResult.GetValue(DelayOption) * 1000;
72✔
197
        Blame = parseResult.GetValue(BlameOption);
72✔
198
        Configuration = parseResult.GetValue(ConfigurationOption);
72✔
199
        Framework = parseResult.GetValue(FrameworkOption);
72✔
200
        Verbosity = parseResult.GetValue(VerbosityOption);
72✔
201
        DeleteReportFiles = parseResult.GetValue(DeleteReportFilesOption);
71✔
202
        Collector = parseResult.GetValue(CollectorOption);
71✔
203
        MergeCoverageFormat = parseResult.GetValue(MergeCoverageFormatOption);
71✔
204
        ExtraArguments = FetchExtraArgumentsFromParse(parseResult);
71✔
205
        InlineRunSettings = FetchInlineRunSettingsFromParse(parseResult);
71✔
206
        EnvironmentVariables = parseResult.GetValue(EnvironmentVariablesOption);
71✔
207
        
208
        //Store Original Values
209
        OriginalFilter = Filter;
71✔
210
    }
71✔
211

212
    public string GetTestArgumentList(string resultsDirectory)
213
        => string.Concat("test",
88✔
214
            string.IsNullOrWhiteSpace(Path) ? string.Empty : $" {Path}",
88✔
215
            AddArguments(Filter, FilterOption),
88✔
216
            AddArguments(Settings, SettingsOption),
88✔
217
            AddArguments(Logger, LoggerOption),
88✔
218
            AddArguments(NoBuild, NoBuildOption),
88✔
219
            AddArguments(NoRestore, NoRestoreOption),
88✔
220
            AddArguments(Blame, BlameOption),
88✔
221
            AddArguments(Configuration, ConfigurationOption),
88✔
222
            AddArguments(Framework, FrameworkOption),
88✔
223
            AddArguments(Verbosity, VerbosityOption),
88✔
224
            AddArguments(Collector, CollectorOption),
88✔
225
            AddArguments(EnvironmentVariables, EnvironmentVariablesOption),
88✔
226
            string.IsNullOrWhiteSpace(resultsDirectory) ? resultsDirectory : AddArguments(resultsDirectory, ResultsDirectoryOption),
88✔
227
            GetExtraArguments(),
88✔
228
            InlineRunSettings);
88✔
229
    
230
    public string GetMergeCoverageArgumentList(string fileNames, string resultsDirectory)
231
        => string.Concat("merge ",
6✔
232
            $"-o {System.IO.Path.Combine(resultsDirectory, $"merged.{GetMergeExtension()}")} ",
6✔
233
            $"-f {MergeCoverageFormat} ",
6✔
234
            $"-r {fileNames}");
6✔
235
    
236
    private static string GetOptionName(Option option)
237
    {
238
        // Use short aliases for specific commonly-used options to keep command lines shorter
239
        // Otherwise use the full name for clarity
240
        return option.Name switch
249✔
241
        {
249✔
242
            "--configuration" => "-c",
5✔
243
            "--verbosity" => "-v",
21✔
244
            "--environment" => "-e",
3✔
245
            _ => option.Name
220✔
246
        };
249✔
247
    }
248
    
249
    public string AddArguments<T>(T value, Option<T> option)
250
        => value is not null
88✔
251
            ? $" {GetOptionName(option)} \"{value}\""
88✔
252
            : string.Empty;
88✔
253
    
254
    public string AddArguments(string? value, Option<string> option)
255
        => value is not null
520✔
256
            ? $" {GetOptionName(option)} \"{value}\""
520✔
257
            : string.Empty;
520✔
258
    
259
    public string AddArguments<T>(T value, Option<IEnumerable<T>> option)
260
        => value is not null
86!
261
            ? $" {GetOptionName(option)} \"{value}\""
86✔
262
            : string.Empty;
86✔
263

264
    public string AddArguments<T>(bool value, Option<T> option)
265
        => value
264✔
266
            ? $" {GetOptionName(option)}"
264✔
267
            : string.Empty;
264✔
268
    
269
    public string AddArguments<T>(IEnumerable<T>? values, Option<IEnumerable<T>> option)
270
    {
271
        if (values is null)
176✔
272
            return string.Empty;
5✔
273
        
274
        StringBuilder str = new StringBuilder();
171✔
275
        foreach (var value in values)
514✔
276
            str.Append(AddArguments(value, option));
86✔
277

278
        return str.ToString();
171✔
279
    } 
280

281
    public string AppendFailedTests(string failedTests)
282
        => string.IsNullOrWhiteSpace(OriginalFilter) ? 
57✔
283
            failedTests : 
57✔
284
            string.Concat("(", OriginalFilter, ")", "&(", failedTests, ")");
57✔
285
    
286
    private string GetMergeExtension()
287
        => MergeCoverageFormat switch
6✔
288
        {
6✔
289
            CoverageFormat.Cobertura => "cobertura.xml",
3✔
290
            CoverageFormat.Xml => "xml",
1✔
291
            CoverageFormat.Coverage => "coverage",
1✔
292
            _ => throw new ArgumentOutOfRangeException(nameof(MergeCoverageFormat), MergeCoverageFormat, null)
1✔
293
        };
6✔
294

295
    private string GetExtraArguments()
296
        => string.IsNullOrWhiteSpace(ExtraArguments) ? ExtraArguments : $" {ExtraArguments}";
88✔
297
    
298
    private string FetchExtraArgumentsFromParse(ParseResult parseResult)
299
        => string.Join(' ', parseResult.UnmatchedTokens.Where(IsMsBuildArgument));
71✔
300
    
301
    private static readonly string[] MsBuildArgumentPrefixes = 
2✔
302
    [
2✔
303
        "/p:", "-p:", "--property:", "/property:", "-property:",
2✔
304
        "/m:", "-m:", "/maxCpuCount:", "-maxCpuCount:", "--maxCpuCount:"
2✔
305
    ];
2✔
306
    
307
    private static bool IsMsBuildArgument(string token)
308
        => MsBuildArgumentPrefixes.Any(prefix => token.StartsWith(prefix));
236✔
309
    
310
    private string FetchInlineRunSettingsFromParse(ParseResult parseResult)
311
    {
312
        var inlineSettings = new StringBuilder();
71✔
313
        var inlineSettingsOption = parseResult.GetValue(InlineRunSettingsOption);
71✔
314
        
315
        // Get all unmatched tokens that are not MSBuild arguments
316
        var unmatchedNonMsBuildTokens = parseResult.UnmatchedTokens.Where(t => !IsMsBuildArgument(t)).ToList();
89✔
317

318
        if ((inlineSettingsOption is not null &&
71✔
319
            inlineSettingsOption.Length > 0) ||
71✔
320
            unmatchedNonMsBuildTokens.Count > 0)
71✔
321
        {
322
            inlineSettings.Append(" -- ");
1✔
323
            inlineSettings.Append(string.Join(" ", inlineSettingsOption ?? []));
1!
324
            if (unmatchedNonMsBuildTokens.Count > 0)
1!
325
            {
NEW
326
                inlineSettings.Append(' ');
×
NEW
327
                inlineSettings.Append(string.Join(" ", unmatchedNonMsBuildTokens));
×
328
            }
329
        }
330

331
        return inlineSettings.ToString().Replace("\"", "\\\"");
71✔
332
    } 
333
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc