diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 53d76e7410..c98480ebea 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Text; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; @@ -75,6 +76,13 @@ public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse { (bool isSuccess, IConfig config, CommandLineOptions options) result = default; + var (expandSuccess, expandedArgs) = ExpandResponseFile(args, logger); + if (!expandSuccess) + { + return (false, default, default); + } + + args = expandedArgs; using (var parser = CreateParser(logger)) { parser @@ -86,6 +94,115 @@ public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse return result; } + private static (bool Success, string[] ExpandedTokens) ExpandResponseFile(string[] args, ILogger logger) + { + List result = new (); + foreach (var arg in args) + { + if (arg.StartsWith("@")) + { + var fileName = arg.Substring(1); + try + { + if (File.Exists(fileName)) + { + var lines = File.ReadAllLines(fileName); + foreach (var line in lines) + { + result.AddRange(ConsumeTokens(line)); + } + } + else + { + logger.WriteLineError($"Response file {fileName} does not exists."); + return (false, Array.Empty()); + } + } + catch (Exception ex) + { + logger.WriteLineError($"Failed to parse RSP file: {fileName}, {ex.Message}"); + return (false, Array.Empty()); + } + } + else + { + if (arg.Contains(' ')) + { + // Workaround for CommandLine library issue with parsing these kind of args. + result.Add(" " + arg); + } + else + { + result.Add(arg); + } + } + } + + return (true, result.ToArray()); + } + + private static IEnumerable ConsumeTokens(string line) + { + bool insideQuotes = false; + var token = new StringBuilder(); + for (int i = 0; i < line.Length; i++) + { + char currentChar = line[i]; + if (currentChar == ' ' && !insideQuotes) + { + if (token.Length > 0) + { + yield return GetToken(); + token = new StringBuilder(); + } + + continue; + } + + if (currentChar == '"') + { + insideQuotes = !insideQuotes; + continue; + } + + if (currentChar == '\\' && insideQuotes) + { + if (line[i + 1] == '"') + { + insideQuotes = false; + i++; + continue; + } + + if (line[i + 1] == '\\') + { + token.Append('\\'); + i++; + continue; + } + } + + token.Append(currentChar); + } + + if (token.Length > 0) + { + yield return GetToken(); + } + + string GetToken() + { + var result = token.ToString(); + if (result.Contains(' ')) + { + // Workaround for CommandLine library issue with parsing these kind of args. + return " " + result; + } + + return result; + } + } + internal static bool TryUpdateArgs(string[] args, out string[]? updatedArgs, Action updater) { (bool isSuccess, CommandLineOptions options) result = default; diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index f6f60716bd..9d012a468f 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -587,6 +587,38 @@ public void UsersCanSpecifyWithoutOverheadEvalution() } } + [Fact] + public void UserCanSpecifyWasmArgs() + { + var parsedConfiguration = ConfigParser.Parse(new[] { "--runtimes", "wasm", "--wasmArgs", "--expose_wasm --module" }, new OutputLogger(Output)); + Assert.True(parsedConfiguration.isSuccess); + var jobs = parsedConfiguration.config.GetJobs(); + foreach (var job in parsedConfiguration.config.GetJobs()) + { + var wasmRuntime = Assert.IsType(job.Environment.Runtime); + Assert.Equal(" --expose_wasm --module", wasmRuntime.JavaScriptEngineArguments); + } + } + + [Fact] + public void UserCanSpecifyWasmArgsViaResponseFile() + { + var tempResponseFile = Path.GetRandomFileName(); + File.WriteAllLines(tempResponseFile, new[] + { + "--runtimes wasm", + "--wasmArgs \"--expose_wasm --module\"" + }); + var parsedConfiguration = ConfigParser.Parse(new[] { $"@{tempResponseFile}" }, new OutputLogger(Output)); + Assert.True(parsedConfiguration.isSuccess); + var jobs = parsedConfiguration.config.GetJobs(); + foreach (var job in parsedConfiguration.config.GetJobs()) + { + var wasmRuntime = Assert.IsType(job.Environment.Runtime); + Assert.Equal(" --expose_wasm --module", wasmRuntime.JavaScriptEngineArguments); + } + } + [Theory] [InlineData("--filter abc", "--filter *")] [InlineData("-f abc", "--filter *")]