1 // ***********************************************************************
2 // Copyright (c) 2015 Charlie Poole
4 // Permission is hereby granted, free of charge, to any person obtaining
5 // a copy of this software and associated documentation files (the
6 // "Software"), to deal in the Software without restriction, including
7 // without limitation the rights to use, copy, modify, merge, publish,
8 // distribute, sublicense, and/or sell copies of the Software, and to
9 // permit persons to whom the Software is furnished to do so, subject to
10 // the following conditions:
12 // The above copyright notice and this permission notice shall be
13 // included in all copies or substantial portions of the Software.
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 // ***********************************************************************
25 #define NUNIT_FRAMEWORK
30 using System.Collections.Generic;
34 namespace NUnit.Common
37 /// CommandLineOptions is the base class the specific option classes
38 /// used for nunit3-console and nunitlite. It encapsulates all common
39 /// settings and features of both. This is done to ensure that common
40 /// features remain common and for the convenience of having the code
41 /// in a common location. The class inherits from the Mono
42 /// Options OptionSet class and provides a central location
43 /// for defining and parsing options.
45 public class CommandLineOptions : OptionSet
47 private bool validated;
49 private bool noresult;
54 internal CommandLineOptions(IDefaultOptionsProvider defaultOptionsProvider, params string[] args)
56 // Apply default options
57 if (defaultOptionsProvider == null) throw new ArgumentNullException("defaultOptionsProvider");
59 TeamCity = defaultOptionsProvider.TeamCity;
67 public CommandLineOptions(params string[] args)
80 public bool Explore { get; private set; }
82 public bool ShowHelp { get; private set; }
84 public bool ShowVersion { get; private set; }
88 private List<string> inputFiles = new List<string>();
89 public IList<string> InputFiles { get { return inputFiles; } }
91 private List<string> testList = new List<string>();
92 public IList<string> TestList { get { return testList; } }
94 public string TestParameters { get; private set; }
96 public string WhereClause { get; private set; }
97 public bool WhereClauseSpecified { get { return WhereClause != null; } }
99 private int defaultTimeout = -1;
100 public int DefaultTimeout { get { return defaultTimeout; } }
101 public bool DefaultTimeoutSpecified { get { return defaultTimeout >= 0; } }
103 private int randomSeed = -1;
104 public int RandomSeed { get { return randomSeed; } }
105 public bool RandomSeedSpecified { get { return randomSeed >= 0; } }
107 public string DefaultTestNamePattern { get; private set; }
109 private int numWorkers = -1;
110 public int NumberOfTestWorkers { get { return numWorkers; } }
111 public bool NumberOfTestWorkersSpecified { get { return numWorkers >= 0; } }
113 public bool StopOnError { get; private set; }
115 public bool WaitBeforeExit { get; private set; }
119 public bool NoHeader { get; private set; }
121 public bool NoColor { get; private set; }
123 public bool Verbose { get; private set; }
125 public bool TeamCity { get; private set; }
127 public string OutFile { get; private set; }
128 public bool OutFileSpecified { get { return OutFile != null; } }
130 public string ErrFile { get; private set; }
131 public bool ErrFileSpecified { get { return ErrFile != null; } }
133 public string DisplayTestLabels { get; private set; }
136 private string workDirectory = null;
137 public string WorkDirectory
139 get { return workDirectory ?? NUnit.Env.DefaultWorkDirectory; }
141 public bool WorkDirectorySpecified { get { return workDirectory != null; } }
144 public string InternalTraceLevel { get; private set; }
145 public bool InternalTraceLevelSpecified { get { return InternalTraceLevel != null; } }
147 /// <summary>Indicates whether a full report should be displayed.</summary>
148 public bool Full { get; private set; }
151 private List<OutputSpecification> resultOutputSpecifications = new List<OutputSpecification>();
152 public IList<OutputSpecification> ResultOutputSpecifications
157 return new OutputSpecification[0];
159 if (resultOutputSpecifications.Count == 0)
160 resultOutputSpecifications.Add(new OutputSpecification("TestResult.xml"));
162 return resultOutputSpecifications;
166 private List<OutputSpecification> exploreOutputSpecifications = new List<OutputSpecification>();
167 public IList<OutputSpecification> ExploreOutputSpecifications { get { return exploreOutputSpecifications; } }
171 public List<string> errorMessages = new List<string>();
172 public IList<string> ErrorMessages { get { return errorMessages; } }
176 #region Public Methods
178 public bool Validate()
182 CheckOptionCombinations();
187 return ErrorMessages.Count == 0;
192 #region Helper Methods
194 protected virtual void CheckOptionCombinations()
200 /// Case is ignored when val is compared to validValues. When a match is found, the
201 /// returned value will be in the canonical case from validValues.
203 protected string RequiredValue(string val, string option, params string[] validValues)
205 if (string.IsNullOrEmpty(val))
206 ErrorMessages.Add("Missing required value for option '" + option + "'.");
210 if (validValues != null && validValues.Length > 0)
214 foreach (string valid in validValues)
215 if (string.Compare(valid, val, StringComparison.OrdinalIgnoreCase) == 0)
221 ErrorMessages.Add(string.Format("The value '{0}' is not valid for option '{1}'.", val, option));
226 protected int RequiredInt(string val, string option)
228 // We have to return something even though the value will
229 // be ignored if an error is reported. The -1 value seems
230 // like a safe bet in case it isn't ignored due to a bug.
233 if (string.IsNullOrEmpty(val))
234 ErrorMessages.Add("Missing required value for option '" + option + "'.");
237 // NOTE: Don't replace this with TryParse or you'll break the CF build!
240 result = int.Parse(val);
244 ErrorMessages.Add("An int value was expected for option '{0}' but a value of '{1}' was used");
251 private string ExpandToFullPath(string path)
253 if (path == null) return null;
255 #if NETCF || PORTABLE
256 return Path.Combine(NUnit.Env.DocumentFolder, path);
258 return Path.GetFullPath(path);
262 protected virtual void ConfigureOptions()
264 // NOTE: The order in which patterns are added
265 // determines the display order for the help.
268 this.Add("test=", "Comma-separated list of {NAMES} of tests to run or explore. This option may be repeated.",
269 v => ((List<string>)TestList).AddRange(TestNameParser.Parse(RequiredValue(v, "--test"))));
271 this.Add("testlist=", "File {PATH} containing a list of tests to run, one per line. This option may be repeated.",
274 string testListFile = RequiredValue(v, "--testlist");
276 var fullTestListPath = ExpandToFullPath(testListFile);
278 if (!File.Exists(fullTestListPath))
279 ErrorMessages.Add("Unable to locate file: " + testListFile);
284 using (var rdr = new StreamReader(fullTestListPath))
286 while (!rdr.EndOfStream)
288 var line = rdr.ReadLine().Trim();
290 if (!string.IsNullOrEmpty(line) && line[0] != '#')
291 ((List<string>)TestList).Add(line);
297 ErrorMessages.Add("Unable to read file: " + testListFile);
303 this.Add("where=", "Test selection {EXPRESSION} indicating what tests will be run. See description below.",
304 v => WhereClause = RequiredValue(v, "--where"));
306 this.Add("params|p=", "Define a test parameter.",
309 string parameters = RequiredValue( v, "--params");
311 foreach (string param in parameters.Split(new[] { ';' }))
313 if (!param.Contains("="))
314 ErrorMessages.Add("Invalid format for test parameter. Use NAME=VALUE.");
317 if (TestParameters == null)
318 TestParameters = parameters;
320 TestParameters += ";" + parameters;
323 this.Add("timeout=", "Set timeout for each test case in {MILLISECONDS}.",
324 v => defaultTimeout = RequiredInt(v, "--timeout"));
326 this.Add("seed=", "Set the random {SEED} used to generate test cases.",
327 v => randomSeed = RequiredInt(v, "--seed"));
329 this.Add("workers=", "Specify the {NUMBER} of worker threads to be used in running tests. If not specified, defaults to 2 or the number of processors, whichever is greater.",
330 v => numWorkers = RequiredInt(v, "--workers"));
332 this.Add("stoponerror", "Stop run immediately upon any test failure or error.",
333 v => StopOnError = v != null);
335 this.Add("wait", "Wait for input before closing console window.",
336 v => WaitBeforeExit = v != null);
339 this.Add("work=", "{PATH} of the directory to use for output files. If not specified, defaults to the current directory.",
340 v => workDirectory = RequiredValue(v, "--work"));
342 this.Add("output|out=", "File {PATH} to contain text output from the tests.",
343 v => OutFile = RequiredValue(v, "--output"));
345 this.Add("err=", "File {PATH} to contain error output from the tests.",
346 v => ErrFile = RequiredValue(v, "--err"));
348 this.Add("full", "Prints full report of all test results.",
349 v => Full = v != null);
351 this.Add("result=", "An output {SPEC} for saving the test results.\nThis option may be repeated.",
352 v => resultOutputSpecifications.Add(new OutputSpecification(RequiredValue(v, "--resultxml"))));
354 this.Add("explore:", "Display or save test info rather than running tests. Optionally provide an output {SPEC} for saving the test info. This option may be repeated.", v =>
358 ExploreOutputSpecifications.Add(new OutputSpecification(v));
361 this.Add("noresult", "Don't save any test results.",
362 v => noresult = v != null);
364 this.Add("labels=", "Specify whether to write test case names to the output. Values: Off, On, All",
365 v => DisplayTestLabels = RequiredValue(v, "--labels", "Off", "On", "All"));
367 this.Add("test-name-format=", "Non-standard naming pattern to use in generating test names.",
368 v => DefaultTestNamePattern = RequiredValue(v, "--test-name-format"));
371 this.Add("trace=", "Set internal trace {LEVEL}.\nValues: Off, Error, Warning, Info, Verbose (Debug)",
372 v => InternalTraceLevel = RequiredValue(v, "--trace", "Off", "Error", "Warning", "Info", "Verbose", "Debug"));
375 this.Add("teamcity", "Turns on use of TeamCity service messages.",
376 v => TeamCity = v != null);
379 this.Add("noheader|noh", "Suppress display of program information at start of run.",
380 v => NoHeader = v != null);
382 this.Add("nocolor|noc", "Displays console output without color.",
383 v => NoColor = v != null);
385 this.Add("verbose|v", "Display additional information as the test runs.",
386 v => Verbose = v != null);
388 this.Add("help|h", "Display this message and exit.",
389 v => ShowHelp = v != null);
391 this.Add("version|V", "Display the header and exit.",
392 v => ShowVersion = v != null);
398 if (v.StartsWith("-") || v.StartsWith("/") && Environment.NewLine == "\r\n")
400 if (v.StartsWith("-") || v.StartsWith("/") && Path.DirectorySeparatorChar != '/')
402 ErrorMessages.Add("Invalid argument: " + v);