From a419a8313e1ab22868f2948dbeec4d4d57cdb2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Fri, 12 Jun 2020 16:00:03 -0700 Subject: [PATCH] Adding schema validation (#35) --- azure-pipelines.yml | 103 ++++++------- .../JobConnection.cs | 103 ++++++------- .../JsonTypeResolution.cs | 35 +++++ .../Microsoft.Crank.Controller.csproj | 11 +- src/Microsoft.Crank.Controller/Program.cs | 137 +++++++++--------- src/Microsoft.Crank.Jobs.Wrk/wrk.yml | 3 + src/Microsoft.Crank.Models/Job.cs | 2 + 7 files changed, 216 insertions(+), 178 deletions(-) create mode 100644 src/Microsoft.Crank.Controller/JsonTypeResolution.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 37d9103f1..903b159a2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,7 +25,7 @@ trigger: - release/* pr: - autoCancel: false + autoCancel: true branches: include: - '*' @@ -44,57 +44,58 @@ stages: enableTelemetry: true mergeTestResults: true jobs: - - job: Windows - pool: - ${{ if eq(variables['System.TeamProject'], 'public') }}: - name: NetCorePublic-Pool - queue: BuildPool.Server.Amd64.VS2019.Open - ${{ if ne(variables['System.TeamProject'], 'public') }}: - name: NetCoreInternal-Pool - queue: BuildPool.Server.Amd64.VS2019 - variables: - + - ${{ if and(eq(variables['System.TeamProject'], 'internal'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - job: Windows + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: NetCorePublic-Pool + queue: BuildPool.Server.Amd64.VS2019.Open + ${{ if ne(variables['System.TeamProject'], 'public') }}: + name: NetCoreInternal-Pool + queue: BuildPool.Server.Amd64.VS2019 + variables: + - # Only enable publishing in official builds. - - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT - - group: Publish-Build-Assets - - name: _OfficialBuildArgs - value: /p:DotNetSignType=$(_SignType) - /p:TeamName=$(_TeamName) - /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines) - /p:OfficialBuildId=$(BUILD.BUILDNUMBER) - - name: _SignType - value: real - # else - - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}: - - name: _OfficialBuildArgs - value: '' - - name: _SignType - value: test - - steps: - - checkout: self - clean: true - - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs) - displayName: Build and Publish - - task: PublishBuildArtifacts@1 - displayName: Upload TestResults - condition: always() - continueOnError: true - inputs: - pathtoPublish: artifacts/TestResults/$(_BuildConfig)/ - artifactName: $(Agent.Os)_$(Agent.JobName) TestResults - artifactType: Container - parallel: true - - task: PublishBuildArtifacts@1 - displayName: Upload package artifacts - condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release')) - inputs: - pathtoPublish: artifacts/packages/ - artifactName: artifacts - artifactType: Container - parallel: true + # Only enable publishing in official builds. + - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT + - group: Publish-Build-Assets + - name: _OfficialBuildArgs + value: /p:DotNetSignType=$(_SignType) + /p:TeamName=$(_TeamName) + /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines) + /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + - name: _SignType + value: real + # else + - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}: + - name: _OfficialBuildArgs + value: '' + - name: _SignType + value: test + + steps: + - checkout: self + clean: true + - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs) + displayName: Build and Publish + - task: PublishBuildArtifacts@1 + displayName: Upload TestResults + condition: always() + continueOnError: true + inputs: + pathtoPublish: artifacts/TestResults/$(_BuildConfig)/ + artifactName: $(Agent.Os)_$(Agent.JobName) TestResults + artifactType: Container + parallel: true + - task: PublishBuildArtifacts@1 + displayName: Upload package artifacts + condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release')) + inputs: + pathtoPublish: artifacts/packages/ + artifactName: artifacts + artifactType: Container + parallel: true - job: Ubuntu_16_04 displayName: 'Ubuntu 16.04' diff --git a/src/Microsoft.Crank.Controller/JobConnection.cs b/src/Microsoft.Crank.Controller/JobConnection.cs index 643cd8a1a..52405faae 100644 --- a/src/Microsoft.Crank.Controller/JobConnection.cs +++ b/src/Microsoft.Crank.Controller/JobConnection.cs @@ -56,9 +56,7 @@ public JobConnection(Job definition, Uri serverUri) public Job Job { get; private set; } public async Task StartAsync( - string jobName, - CommandOption _outputArchiveOption, - CommandOption _buildArchiveOption + string jobName ) { _jobName = jobName; @@ -152,80 +150,75 @@ CommandOption _buildArchiveOption } // Upload custom package contents - if (_outputArchiveOption.HasValue()) + foreach (var outputArchiveValue in Job.Options.OutputArchives) { - foreach (var outputArchiveValue in _outputArchiveOption.Values) - { - var outputFileSegments = outputArchiveValue.Split(';', 2, StringSplitOptions.RemoveEmptyEntries); + var outputFileSegments = outputArchiveValue.Split(';', 2, StringSplitOptions.RemoveEmptyEntries); - string localArchiveFilename = outputFileSegments[0]; + string localArchiveFilename = outputFileSegments[0]; - var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - if (Directory.Exists(tempFolder)) - { - Directory.Delete(tempFolder, true); - } + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, true); + } - Directory.CreateDirectory(tempFolder); + Directory.CreateDirectory(tempFolder); - _temporaryFolders.Add(tempFolder); + _temporaryFolders.Add(tempFolder); - // Download the archive, while pinging the server to keep the job alive - if (outputArchiveValue.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - localArchiveFilename = await DownloadTemporaryFileAsync(localArchiveFilename, _serverJobUri); - } + // Download the archive, while pinging the server to keep the job alive + if (outputArchiveValue.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + localArchiveFilename = await DownloadTemporaryFileAsync(localArchiveFilename, _serverJobUri); + } - ZipFile.ExtractToDirectory(localArchiveFilename, tempFolder); + ZipFile.ExtractToDirectory(localArchiveFilename, tempFolder); - if (outputFileSegments.Length > 1) - { - Job.Options.OutputFiles.Add(Path.Combine(tempFolder, "*.*") + ";" + outputFileSegments[1]); - } - else - { - Job.Options.OutputFiles.Add(Path.Combine(tempFolder, "*.*")); - } + if (outputFileSegments.Length > 1) + { + Job.Options.OutputFiles.Add(Path.Combine(tempFolder, "*.*") + ";" + outputFileSegments[1]); + } + else + { + Job.Options.OutputFiles.Add(Path.Combine(tempFolder, "*.*")); } } + // Upload custom build package contents - if (_buildArchiveOption.HasValue()) + foreach (var buildArchiveValue in Job.Options.BuildArchives) { - foreach (var buildArchiveValue in _buildArchiveOption.Values) - { - var buildFileSegments = buildArchiveValue.Split(';', 2, StringSplitOptions.RemoveEmptyEntries); + var buildFileSegments = buildArchiveValue.Split(';', 2, StringSplitOptions.RemoveEmptyEntries); - string localArchiveFilename = buildFileSegments[0]; + string localArchiveFilename = buildFileSegments[0]; - var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - if (Directory.Exists(tempFolder)) - { - Directory.Delete(tempFolder, true); - } + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, true); + } - Directory.CreateDirectory(tempFolder); + Directory.CreateDirectory(tempFolder); - _temporaryFolders.Add(tempFolder); + _temporaryFolders.Add(tempFolder); - // Download the archive, while pinging the server to keep the job alive - if (buildArchiveValue.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - localArchiveFilename = await DownloadTemporaryFileAsync(localArchiveFilename, _serverJobUri); - } + // Download the archive, while pinging the server to keep the job alive + if (buildArchiveValue.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + localArchiveFilename = await DownloadTemporaryFileAsync(localArchiveFilename, _serverJobUri); + } - ZipFile.ExtractToDirectory(localArchiveFilename, tempFolder); + ZipFile.ExtractToDirectory(localArchiveFilename, tempFolder); - if (buildFileSegments.Length > 1) - { - Job.Options.BuildFiles.Add(Path.Combine(tempFolder, "*.*") + ";" + buildFileSegments[1]); - } - else - { - Job.Options.BuildFiles.Add(Path.Combine(tempFolder, "*.*")); - } + if (buildFileSegments.Length > 1) + { + Job.Options.BuildFiles.Add(Path.Combine(tempFolder, "*.*") + ";" + buildFileSegments[1]); + } + else + { + Job.Options.BuildFiles.Add(Path.Combine(tempFolder, "*.*")); } } diff --git a/src/Microsoft.Crank.Controller/JsonTypeResolution.cs b/src/Microsoft.Crank.Controller/JsonTypeResolution.cs new file mode 100644 index 000000000..24919802b --- /dev/null +++ b/src/Microsoft.Crank.Controller/JsonTypeResolution.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Microsoft.Crank.Controller +{ + /// Provides types resolution for YAML + /// Without this booleans and numbers are parsed as strings + public class JsonTypeResolver : INodeTypeResolver + { + public bool Resolve(NodeEvent nodeEvent, ref Type currentType) + { + if (nodeEvent is Scalar scalar && scalar.IsPlainImplicit) + { + if (decimal.TryParse(scalar.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + { + currentType = typeof(decimal); + return true; + } + else if (bool.TryParse(scalar.Value, out var b)) + { + currentType = typeof(bool); + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.Crank.Controller/Microsoft.Crank.Controller.csproj b/src/Microsoft.Crank.Controller/Microsoft.Crank.Controller.csproj index acefcec2d..9e53b71e0 100644 --- a/src/Microsoft.Crank.Controller/Microsoft.Crank.Controller.csproj +++ b/src/Microsoft.Crank.Controller/Microsoft.Crank.Controller.csproj @@ -16,8 +16,9 @@ - - + + + @@ -25,4 +26,10 @@ + + + + + + diff --git a/src/Microsoft.Crank.Controller/Program.cs b/src/Microsoft.Crank.Controller/Program.cs index adb28d492..949df1ac0 100644 --- a/src/Microsoft.Crank.Controller/Program.cs +++ b/src/Microsoft.Crank.Controller/Program.cs @@ -17,17 +17,17 @@ using McMaster.Extensions.CommandLineUtils; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; using Newtonsoft.Json.Serialization; using NuGet.Versioning; using YamlDotNet.Serialization; using System.Reflection; +using System.Text; namespace Microsoft.Crank.Controller { public class Program { - private static TimeSpan _timeout = TimeSpan.FromMinutes(5); - private static readonly HttpClient _httpClient; private static readonly HttpClientHandler _httpClientHandler; @@ -40,9 +40,6 @@ public class Program private const string _defaultTraceArguments = "BufferSizeMB=1024;CircularMB=1024;clrEvents=JITSymbols;kernelEvents=process+thread+ImageLoad+Profile"; private static CommandOption - _outputArchiveOption, - _buildArchiveOption, - _configOption, _scenarioOption, _jobOption, @@ -115,9 +112,9 @@ public static int Main(string[] args) var app = new CommandLineApplication() { - Name = "BenchmarksDriver", - FullName = "ASP.NET Benchmark Driver", - Description = "Driver for ASP.NET Benchmarks", + Name = "Crank", + FullName = "ASP.NET Benchmarks Controller", + Description = "Crank orchestrates benchmark jobs on Crank agents.", ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, OptionsComparison = StringComparison.OrdinalIgnoreCase, }; @@ -173,42 +170,12 @@ public static int Main(string[] args) "Quiet output, only the results are displayed", CommandOptionType.NoValue); var iterationsOption = app.Option("-i|--iterations", "The number of iterations.", CommandOptionType.SingleValue); - var excludeOption = app.Option("-x|--exclude", - "The number of best and worst jobs to skip.", CommandOptionType.SingleValue); - var shutdownOption = app.Option("--before-shutdown", - "An endpoint to call before the application has shut down.", CommandOptionType.SingleValue); - var benchmarkdotnetOption = app.Option("--benchmarkdotnet", - "Runs a BenchmarkDotNet application, with an optional filter. e.g., --benchmarkdotnet, --benchmarkdotnet:*MyBenchmark*", CommandOptionType.SingleOrNoValue); - - // ServerJob Options - var databaseOption = app.Option("--database", - "The type of database to run the benchmarks with (PostgreSql, SqlServer or MySql). Default is None.", CommandOptionType.SingleValue); - - _outputArchiveOption = app.Option("--output-archive", - "Output archive attachment. Format is 'path[;destination]'. Path can be a URL. e.g., " + - "\"--output-archive c:\\build\\Microsoft.AspNetCore.Mvc.zip\", " + - "\"--output-archive http://raw/github.com/pictures.zip;wwwroot\\pictures\"", - CommandOptionType.MultipleValue); - _buildArchiveOption = app.Option("--build-archive", - "Build archive attachment. Format is 'path[;destination]'. Path can be a URL. e.g., " + - "\"--build-archive c:\\build\\Microsoft.AspNetCore.Mvc.zip\", " + - "\"--build-archive http://raw/github.com/pictures.zip;wwwroot\\pictures\"", - CommandOptionType.MultipleValue); - var buildArguments = app.Option("-ba|--build-arg", - "Defines custom build arguments to use with the benchmarked application e.g., -b \"/p:foo=bar\" --build-arg \"quiet\"", CommandOptionType.MultipleValue); - var serverTimeoutOption = app.Option("--server-timeout", - "Timeout for server jobs. e.g., 00:05:00", CommandOptionType.SingleValue); app.OnExecuteAsync(async (t) => { Log.IsQuiet = quietOption.HasValue(); Log.IsVerbose = verboseOption.HasValue(); - if (serverTimeoutOption.HasValue()) - { - TimeSpan.TryParse(serverTimeoutOption.Value(), out _timeout); - } - var session = _sessionOption.Value(); var iterations = 1; var exclude = 0; @@ -255,7 +222,15 @@ public static int Main(string[] args) if (!_scenarioOption.HasValue() && !_compareOption.HasValue() && !_jobOption.HasValue()) { - Console.Error.WriteLine("No jobs were found. Are you missing the --scenario argument?"); + if (!_configOption.HasValue()) + { + app.ShowHelp(); + } + else + { + Console.Error.WriteLine("No jobs were found. Are you missing the --scenario argument?"); + } + return 1; } @@ -388,7 +363,6 @@ public static int Main(string[] args) session, iterations, exclude, - shutdownOption.Value(), span ); } @@ -453,7 +427,6 @@ private static async Task Run( string session, int iterations, int exclude, - string shutdownEndpoint, TimeSpan span ) { @@ -504,22 +477,18 @@ TimeSpan span jobsByDependency[jobName] = jobs; // Check os and architecture requirements - if (! await EnsureServerRequirementsAsync(jobs, service)) + if (!await EnsureServerRequirementsAsync(jobs, service)) { Log.Write($"Scenario skipped as the agent doesn't match the operating and architecture constraints for '{jobName}' ({String.Join("/", new[] { service.Options.RequiredArchitecture, service.Options.RequiredOperatingSystem })})"); - return new ExecutionResult(); - } + return new ExecutionResult(); + } // Start this service on all configured agent endpoints await Task.WhenAll( jobs.Select(job => { // Start job on agent - return job.StartAsync( - jobName, - _outputArchiveOption, - _buildArchiveOption - ); + return job.StartAsync(jobName); }) ); @@ -833,15 +802,14 @@ TimeSpan span var job = new JobConnection(service, new Uri(service.Endpoints.First())); // Check os and architecture requirements - if (!await EnsureServerRequirementsAsync(new [] { job } , service)) - { + if (!await EnsureServerRequirementsAsync(new[] { job }, service)) + { Log.Write($"Scenario skipped as the agent doesn't match the operating and architecture constraints for '{jobName}' ({String.Join("/", new[] { service.Options.RequiredArchitecture, service.Options.RequiredOperatingSystem })})"); - return new ExecutionResult(); - } - + return new ExecutionResult(); + } // Start this service on the configured agent endpoint - await job.StartAsync(jobName, _outputArchiveOption, _buildArchiveOption); + await job.StartAsync(jobName); // Start threads that will keep the jobs alive job.StartKeepAlive(); @@ -855,9 +823,9 @@ TimeSpan span await job.TryUpdateJobAsync(); - var stop = - job.Job.State == JobState.Stopped || - job.Job.State == JobState.Deleted || + var stop = + job.Job.State == JobState.Stopped || + job.Job.State == JobState.Deleted || job.Job.State == JobState.Failed ; @@ -1107,7 +1075,7 @@ IEnumerable profiles } } - // Roundtrip the JObject such that it contains all the exta properties of the Configuration class that are not in the configuration file + // Roundtrip the JObject such that it contains all the extra properties of the Configuration class that are not in the configuration file var configurationInstance = configuration.ToObject(); // After that point we only modify the concrete instance of Configuration @@ -1301,7 +1269,7 @@ public static async Task LoadConfigurationAsync(string configurationFil } else { - configurationExtension = Path.GetExtension(configurationFilenameOrUrl); + configurationExtension = Path.GetExtension(configurationFilenameOrUrl); } switch (configurationExtension) @@ -1313,7 +1281,10 @@ public static async Task LoadConfigurationAsync(string configurationFil case ".yml": case ".yaml": - var deserializer = new DeserializerBuilder().Build(); + var deserializer = new DeserializerBuilder() + .WithNodeTypeResolver(new JsonTypeResolver()) + .Build(); + var yamlObject = deserializer.Deserialize(new StringReader(configurationContent)); var serializer = new SerializerBuilder() @@ -1321,7 +1292,37 @@ public static async Task LoadConfigurationAsync(string configurationFil .Build(); var json = serializer.Serialize(yamlObject); + // Format json in case the schema validation fails and we need to render error line numbers + localconfiguration = JObject.Parse(json); + localconfiguration.AddFirst(new JProperty("$schema", "https://raw.githubusercontent.com/aspnet/Benchmarks/master/src/BenchmarksDriver2/benchmarks.schema.json")); + json = localconfiguration.ToString(Formatting.Indented); localconfiguration = JObject.Parse(json); + + var schemaJson = File.ReadAllText(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "benchmarks.schema.json")); + var schema = JSchema.Parse(schemaJson); + bool valid = localconfiguration.IsValid(schema, out IList errorMessages); + + if (!valid) + { + var validationFilename = Path.Combine(Path.GetTempPath(), "crank-debug.json"); + File.WriteAllText(validationFilename, json); + + var lines = json.Split(new [] { '\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); + + var errorBuilder = new StringBuilder(); + + errorBuilder.AppendLine($"Invalid configuration file '{configurationFilenameOrUrl}'"); + errorBuilder.AppendLine($"Debug file created at '{validationFilename}"); + + foreach (var error in errorMessages) + { + errorBuilder.AppendLine($"at [{error.LineNumber}, {error.LinePosition}]: {error.Message}"); + errorBuilder.AppendLine($" {lines[error.LineNumber - 1]}"); + } + + throw new ControllerException(errorBuilder.ToString()); + } + break; default: throw new ControllerException($"Unsupported configuration format: {configurationExtension}"); @@ -1335,7 +1336,7 @@ public static async Task LoadConfigurationAsync(string configurationFil var jobObject = (JObject)job.Value; if (jobObject.ContainsKey("source")) { - var source = (JObject)jobObject["source"]; + var source = (JObject)jobObject["source"]; if (source.ContainsKey("localFolder")) { var localFolder = source["localFolder"].ToString(); @@ -1344,14 +1345,10 @@ public static async Task LoadConfigurationAsync(string configurationFil { var configurationFilename = new FileInfo(configurationFilenameOrUrl).FullName; var resolvedFilename = new FileInfo(Path.Combine(Path.GetDirectoryName(configurationFilename), localFolder)).FullName; - + source["localFolder"] = resolvedFilename; } } - else - { - Log.Write(source.ToString()); - } } } } @@ -1465,7 +1462,7 @@ private static Func, double> Percentile(int percentile) if (orderedList.Length > nth) { - return orderedList[nth]; + return orderedList[nth]; } else { @@ -1814,7 +1811,7 @@ private static void WriteMeasures(JobConnection job) } } } - + private static async Task CheckUpdateAsync() { var packageVersionUrl = "https://api.nuget.org/v3-flatcontainer/microsoft.crank.controller/index.json"; @@ -1827,7 +1824,7 @@ private static async Task CheckUpdateAsync() var last = versions.FirstOrDefault().ToString(); var attribute = Assembly.GetExecutingAssembly().GetCustomAttribute(); - + if (new NuGetVersion(last) > new NuGetVersion(attribute.InformationalVersion)) { Console.ForegroundColor = ConsoleColor.DarkYellow; diff --git a/src/Microsoft.Crank.Jobs.Wrk/wrk.yml b/src/Microsoft.Crank.Jobs.Wrk/wrk.yml index d9b727c7d..5e13e9ac3 100644 --- a/src/Microsoft.Crank.Jobs.Wrk/wrk.yml +++ b/src/Microsoft.Crank.Jobs.Wrk/wrk.yml @@ -23,7 +23,10 @@ jobs: pipeline: 1 script: null scriptArguments: null + script: '' # path or url of a LUA script, e.g., https://raw.githubusercontent.com/wg/wrk/master/scripts/post.lua + scriptArguments: '' # arguments passed to the script, e.g., 16 requiredVariables: serverUri: http://10.0.0.102 serverPort: 5000 arguments: "-c {{connections}} {{serverUri}}:{{serverPort}}{{path}} --latency -d {{duration}}s -w {{warmup}}s -t {{threads}} {{headers[presetHeaders]}} {% if pipeline > 1 %} -s scripts/pipeline.lua -- {{ pipeline }} {%elsif script %} -s {{script}} -- {{scriptArguments}} {% endif %}" + arguments: "-c {{connections}} {{serverUri}}:{{serverPort}}{{path}} --latency -d {{duration}}s -w {{warmup}}s -t {{threads}} {{headers[presetHeaders]}} {% if pipeline > 1 %} -s scripts/pipeline.lua -- {{ pipeline }} {% elsif script.length > 0 %} -s {{script}} -- {{scriptArguments}} {% endif %}" diff --git a/src/Microsoft.Crank.Models/Job.cs b/src/Microsoft.Crank.Models/Job.cs index 12b146b18..5eaeb88b2 100644 --- a/src/Microsoft.Crank.Models/Job.cs +++ b/src/Microsoft.Crank.Models/Job.cs @@ -197,5 +197,7 @@ public class Options public bool DiscardResults { get; set; } public List BuildFiles { get; set; } = new List(); public List OutputFiles { get; set; } = new List(); + public List BuildArchives { get; set; } = new List(); + public List OutputArchives { get; set; } = new List(); } }