|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.DotNet.HelixTestTasks;
/// <summary>
/// Computes per-batch Helix work item metadata (name, staging directory, timeout).
/// Timeout is <see cref="MinutesPerSuite"/> minutes per suite (default 20) with a
/// <see cref="MinimumMinutes"/> floor (default 30) to handle the heaviest individual
/// suites (e.g. Cryptography ~17 min), capped at <see cref="MaximumMinutes"/>.
/// Slower test scopes (e.g. outerloop) can raise these knobs.
/// </summary>
public class ComputeBatchTimeout : Task
{
[Required]
public ITaskItem[] GroupedItems { get; set; } = Array.Empty<ITaskItem>();
[Required]
public ITaskItem[] BatchIds { get; set; } = Array.Empty<ITaskItem>();
[Required]
public string ItemPrefix { get; set; } = string.Empty;
[Required]
public string BatchOutputDir { get; set; } = string.Empty;
/// <summary>Minutes budgeted per test suite in a batch. Default 20.</summary>
public int MinutesPerSuite { get; set; } = 20;
/// <summary>Minimum per-batch timeout in minutes. Default 30.</summary>
public int MinimumMinutes { get; set; } = 30;
/// <summary>
/// Maximum per-batch timeout in minutes. Default 1439 (capped below 24h to
/// prevent hh format wrapping). Keep this below the AzDO job timeout (minus
/// build/send overhead) so the synchronous send-to-Helix wait cannot outlast
/// the job.
/// </summary>
public int MaximumMinutes { get; set; } = 1439;
[Output]
public ITaskItem[] TimedItems { get; set; } = Array.Empty<ITaskItem>();
public override bool Execute()
{
// Sanitize MSBuild-provided knobs: fall back to defaults on non-positive values, and cap
// MaximumMinutes below 24h so the "hh" specifier in the formatted Timeout cannot wrap
// (e.g. 1440 minutes would render as 00:00:00). Also ensure the floor never exceeds the cap.
// Warn whenever a value is corrected so a pipeline misconfiguration is diagnosable in the log.
if (MinutesPerSuite <= 0)
{
Log.LogWarning("{0} must be positive but was {1}; falling back to default 20.", nameof(MinutesPerSuite), MinutesPerSuite);
MinutesPerSuite = 20;
}
if (MinimumMinutes <= 0)
{
Log.LogWarning("{0} must be positive but was {1}; falling back to default 30.", nameof(MinimumMinutes), MinimumMinutes);
MinimumMinutes = 30;
}
if (MaximumMinutes <= 0 || MaximumMinutes > 1439)
{
Log.LogWarning("{0} must be in the range [1, 1439] but was {1}; falling back to default 1439.", nameof(MaximumMinutes), MaximumMinutes);
MaximumMinutes = 1439;
}
if (MinimumMinutes > MaximumMinutes)
{
Log.LogWarning("{0} ({1}) exceeds {2} ({3}); clamping {0} to {3}.", nameof(MinimumMinutes), MinimumMinutes, nameof(MaximumMinutes), MaximumMinutes);
MinimumMinutes = MaximumMinutes;
}
var counts = new Dictionary<string, int>();
foreach (var item in GroupedItems)
{
string bid = item.GetMetadata("BatchId");
counts.TryGetValue(bid, out int current);
counts[bid] = current + 1;
}
var result = new List<ITaskItem>();
foreach (var batchId in BatchIds)
{
string bid = batchId.ItemSpec;
int count = counts.GetValueOrDefault(bid, 1);
// MinutesPerSuite minutes per suite to account for WASM startup overhead + test
// execution; MinimumMinutes floor handles the heaviest individual suites
// (e.g. Cryptography ~17m); capped at MaximumMinutes (kept below 24h to prevent
// hh format wrapping, and intended to stay under the AzDO job timeout).
// Multiply in long so an outsized MinutesPerSuite override cannot overflow int; the
// result is bounded by MaximumMinutes (< 24h), so casting back to int is safe.
long perSuiteMinutes = (long)count * MinutesPerSuite;
int totalMinutes = (int)Math.Min(MaximumMinutes, Math.Max(MinimumMinutes, perSuiteMinutes));
var ts = TimeSpan.FromMinutes(totalMinutes);
var helixItem = new TaskItem(ItemPrefix + "Batch-" + bid);
helixItem.SetMetadata("BatchDir", BatchOutputDir + "batch-" + bid + "/");
helixItem.SetMetadata("Timeout", ts.ToString(@"hh\:mm\:ss"));
result.Add(helixItem);
}
TimedItems = result.ToArray();
return !Log.HasLoggedErrors;
}
}
|