Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Microsoft.DotNet.XHarness.Android/InstrumentationRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class InstrumentationRunner

// nunit2 one should go away eventually
private static readonly string[] s_xmlOutputVariableNames = { "nunit2-results-path", "test-results-path" };
private const string CoverageResultsPathVariableName = "coverage-results-path";
private const string TestRunSummaryVariableName = "test-execution-summary";
private const string ShortMessageVariableName = "shortMsg";
private const string ProcessCrashedShortMessage = "Process crashed";
Expand Down Expand Up @@ -129,6 +130,21 @@ public ExitCode RunApkInstrumentation(
bool failurePullingFiles = PullResultXMLs(apkPackageName, outputDirectory, resultValues)!;
bool processCrashed = false;

// Pull coverage results if present (non-fatal on failure)
if (resultValues.TryGetValue(CoverageResultsPathVariableName, out string? coveragePath))
{
_logger.LogInformation($"Found coverage results file: '{coveragePath}'");
try
{
_runner.PullFiles(apkPackageName, coveragePath, outputDirectory);
_logger.LogInformation($"Coverage results pulled to '{outputDirectory}'");
}
catch (Exception toLog)
{
_logger.LogWarning(toLog, "Failed to pull coverage results from {filePathOnDevice}. Coverage data may be unavailable.", coveragePath);
}
}

if (resultValues.TryGetValue(TestRunSummaryVariableName, out string? testRunSummary))
{
_logger.LogInformation($"Test execution summary:{Environment.NewLine}{testRunSummary}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class AndroidRunCommandArguments : XHarnessCommandArguments, IAndroidAp
public ExpectedExitCodeArgument ExpectedExitCode { get; } = new((int)Common.CLI.ExitCode.SUCCESS);
public DeviceOutputFolderArgument DeviceOutputFolder { get; } = new();
public WifiArgument Wifi { get; } = new();
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand All @@ -34,5 +35,6 @@ internal class AndroidRunCommandArguments : XHarnessCommandArguments, IAndroidAp
ExpectedExitCode,
DeviceOutputFolder,
Wifi,
EnableCoverage,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class AndroidTestCommandArguments : XHarnessCommandArguments, IAndroidA
public ExpectedExitCodeArgument ExpectedExitCode { get; } = new((int)Common.CLI.ExitCode.SUCCESS);
public DeviceOutputFolderArgument DeviceOutputFolder { get; } = new();
public WifiArgument Wifi { get; } = new();
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand All @@ -39,6 +40,7 @@ internal class AndroidTestCommandArguments : XHarnessCommandArguments, IAndroidA
ExpectedExitCode,
DeviceOutputFolder,
Wifi,
EnableCoverage,
};

public override void Validate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class AppleJustTestCommandArguments : XHarnessCommandArguments, IAppleA
public EnableLldbArgument EnableLldb { get; } = new();
public EnvironmentalVariablesArgument EnvironmentalVariables { get; } = new();
public SignalAppEndArgument SignalAppEnd { get; } = new();
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand All @@ -45,6 +46,7 @@ internal class AppleJustTestCommandArguments : XHarnessCommandArguments, IAppleA
EnableLldb,
SignalAppEnd,
EnvironmentalVariables,
EnableCoverage,
};

public override void Validate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class AppleTestCommandArguments : XHarnessCommandArguments, IAppleAppRu
public EnvironmentalVariablesArgument EnvironmentalVariables { get; } = new();
public ResetSimulatorArgument ResetSimulator { get; } = new();
public SignalAppEndArgument SignalAppEnd { get; } = new();
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand All @@ -46,5 +47,6 @@ internal class AppleTestCommandArguments : XHarnessCommandArguments, IAppleAppRu
SignalAppEnd,
EnvironmentalVariables,
ResetSimulator,
EnableCoverage,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.

namespace Microsoft.DotNet.XHarness.CLI.CommandArguments;

/// <summary>
/// Shared --enable-coverage switch used across all platforms (Android, Apple, WASM).
/// </summary>
internal class EnableCoverageArgument : SwitchArgument
{
public EnableCoverageArgument() : base("enable-coverage", "Enable code coverage collection during test execution", false)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS
public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new();
public WebServerUploadResults WebServerUploadResults { get; } = new();
public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0;
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand Down Expand Up @@ -69,6 +70,7 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS
WebServerUseCors,
WebServerUseCrossOriginPolicy,
WebServerUseDefaultFiles,
EnableCoverage,
};

public override void Validate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr
public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new();
public WebServerUploadResults WebServerUploadResults { get; } = new();
public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0;
public CommandArguments.EnableCoverageArgument EnableCoverage { get; } = new();

protected override IEnumerable<Argument> GetArguments() => new Argument[]
{
Expand All @@ -55,5 +56,6 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr
WebServerUseCors,
WebServerUseCrossOriginPolicy,
WebServerUseDefaultFiles,
EnableCoverage,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ protected override ExitCode InvokeCommand(ILogger logger)
runner.EnableWifi(Arguments.Wifi == WifiStatus.Enable);
}

if (Arguments.EnableCoverage)
{
Arguments.InstrumentationArguments.Value["enable-coverage"] = "true";
}

var instrumentationRunner = new InstrumentationRunner(logger, runner);
return instrumentationRunner.RunApkInstrumentation(
Arguments.PackageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ protected override ExitCode InvokeCommand(ILogger logger)
{
runner.ClearAdbLog();

if (Arguments.EnableCoverage)
{
Arguments.InstrumentationArguments.Value["enable-coverage"] = "true";
}

var instrumentationRunner = new InstrumentationRunner(logger, runner);
exitCode = instrumentationRunner.RunApkInstrumentation(
Arguments.PackageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.Apple;
Expand All @@ -23,8 +25,21 @@ public AppleJustTestCommand(IServiceCollection services) : base("just-test", fal
{
}

protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider, CancellationToken cancellationToken) =>
serviceProvider.GetRequiredService<IJustTestOrchestrator>()
protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var envVars = Arguments.EnvironmentalVariables.Value;

if (Arguments.EnableCoverage)
{
var coverageVars = new List<(string, string)>(envVars)
{
("NUNIT_ENABLE_COVERAGE", "true"),
("NUNIT_COVERAGE_OUTPUT_PATH", "coverage.cobertura.xml"),
};
envVars = coverageVars;
}

return serviceProvider.GetRequiredService<IJustTestOrchestrator>()
.OrchestrateTest(
Arguments.BundleIdentifier,
Arguments.Target,
Expand All @@ -38,7 +53,8 @@ protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider
includeWirelessDevices: Arguments.IncludeWireless,
enableLldb: Arguments.EnableLldb,
signalAppEnd: Arguments.SignalAppEnd,
Arguments.EnvironmentalVariables.Value,
envVars,
PassThroughArguments,
cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.Apple;
Expand All @@ -25,8 +27,24 @@ public AppleTestCommand(IServiceCollection services) : base("test", false, servi
{
}

protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider, CancellationToken cancellationToken) =>
serviceProvider.GetRequiredService<ITestOrchestrator>()
protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var envVars = Arguments.EnvironmentalVariables.Value;

if (Arguments.EnableCoverage)
{
// Inject coverage env vars so the test runner on the device enables coverage.
// Use Documents/coverage.cobertura.xml — the same directory where test results go,
// which the orchestrator already knows how to pull from the app container.
var coverageVars = new List<(string, string)>(envVars)
{
("NUNIT_ENABLE_COVERAGE", "true"),
("NUNIT_COVERAGE_OUTPUT_PATH", "coverage.cobertura.xml"),
};
envVars = coverageVars;
}

return serviceProvider.GetRequiredService<ITestOrchestrator>()
.OrchestrateTest(
Arguments.AppBundlePath,
Arguments.Target,
Expand All @@ -41,7 +59,8 @@ protected override Task<ExitCode> InvokeInternal(ServiceProvider serviceProvider
resetSimulator: Arguments.ResetSimulator,
enableLldb: Arguments.EnableLldb,
signalAppEnd: Arguments.SignalAppEnd,
Arguments.EnvironmentalVariables.Value,
envVars,
PassThroughArguments,
cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ protected override async Task<ExitCode> InvokeInternal(ILogger logger)
var serviceProvider = Services.BuildServiceProvider();
var diagnosticsData = serviceProvider.GetRequiredService<IDiagnosticsData>();

var coverageOutputPath = Arguments.EnableCoverage
? Path.Combine(Arguments.OutputDirectory, "coverage.cobertura.xml")
: null;

var logProcessor = new WasmTestMessagesProcessor(xmlResultsFilePath,
stdoutFilePath,
logger,
Arguments.ErrorPatternsFile,
symbolicator);
symbolicator,
coverageOutputPath);
var runner = new WasmBrowserTestRunner(
Arguments,
PassThroughArguments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,16 @@ protected override async Task<ExitCode> InvokeInternal(ILogger logger)
Arguments.SymbolicatePatternsFileArgument,
logger);

var coverageOutputPath = Arguments.EnableCoverage
? Path.Combine(Arguments.OutputDirectory, "coverage.cobertura.xml")
: null;

var logProcessor = new WasmTestMessagesProcessor(xmlResultsFilePath,
stdoutFilePath,
logger,
Arguments.ErrorPatternsFile,
symbolicator);
symbolicator,
coverageOutputPath);
var logProcessorTask = Task.Run(() => logProcessor.RunAsync(cts.Token));

var processTask = processManager.ExecuteCommandAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
public class WasmTestMessagesProcessor
{
private static Regex xmlRx = new Regex(@"^STARTRESULTXML ([0-9]*) ([^ ]*) ENDRESULTXML", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static Regex coverageRx = new Regex(@"^STARTCOVERAGEXML ([0-9]*) ([^ ]*) ENDCOVERAGEXML", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly StreamWriter _stdoutFileWriter;
private readonly string _xmlResultsFilePath;
private readonly string? _coverageOutputPath;
private static TimeSpan s_logMessagesTimeout = TimeSpan.FromMinutes(2);

private readonly ILogger _logger;
Expand All @@ -38,9 +40,10 @@ public class WasmTestMessagesProcessor
public TaskCompletionSource WasmExitReceivedTcs { get; } = new ();
public int? ForwardedExitCode {get; private set; }

public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null)
public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null, string? coverageOutputPath = null)
{
_xmlResultsFilePath = xmlResultsFilePath;
_coverageOutputPath = coverageOutputPath;
_stdoutFileWriter = File.CreateText(stdoutFilePath);
_stdoutFileWriter.AutoFlush = true;
_logger = logger;
Expand Down Expand Up @@ -184,6 +187,7 @@ private void ProcessMessage(string message, bool isError = false)
line = line.TrimEnd();

var match = xmlRx.Match(line);
var coverageMatch = coverageRx.Match(line);
if (match.Success)
{
var expectedLength = Int32.Parse(match.Groups[1].Value);
Expand All @@ -201,6 +205,26 @@ private void ProcessMessage(string message, bool isError = false)
}
}
}
else if (coverageMatch.Success && _coverageOutputPath != null)
{
var expectedLength = Int32.Parse(coverageMatch.Groups[1].Value);
var bytes = System.Convert.FromBase64String(coverageMatch.Groups[2].Value);
var outputDir = Path.GetDirectoryName(_coverageOutputPath);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}

File.WriteAllBytes(_coverageOutputPath, bytes);
if (bytes.Length == expectedLength)
{
_logger.LogInformation($"Received expected {bytes.Length} bytes of coverage data, saved to {_coverageOutputPath}");
}
else
{
_logger.LogWarning($"Received {bytes.Length} bytes of coverage data but expected {expectedLength}, saved to {_coverageOutputPath}");
}
}
else
{
ScanMessageForErrorPatterns(line);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.IO;
using System.Threading.Tasks;

Expand All @@ -28,5 +29,12 @@ public override async Task RunAsync()
var options = ApplicationOptions.Current;
using TextWriter? resultsFileMaybe = options.EnableXml ? File.CreateText(TestsResultsFinalPath) : null;
await InternalRunAsync(options, Logger, resultsFileMaybe);

if (CoverageResultPath != null)
{
// Report the coverage path via stdout so the Instrumentation class can
// forward it as INSTRUMENTATION_RESULT: coverage-results-path=<path>
Console.WriteLine($"INSTRUMENTATION_RESULT: coverage-results-path={CoverageResultPath}");
}
}
}
Loading