Skip to content
Draft
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
10 changes: 8 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ root = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = false
insert_final_newline = true

# SA1200: Using directives should be placed correctly
dotnet_diagnostic.SA1200.severity = none

[*.cake]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

[*.{xaml,xml,config,manifest}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = false
insert_final_newline = false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,4 @@ cake-build/addins/
cake-build/modules/
build-output/
unity-packager/
test-results/
94 changes: 91 additions & 3 deletions cake-build/helpers/test-retry.cake
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,41 @@ public class TestRetryHelper
var failedTests = new List<string>();

if (!_context.FileExists(resultsPath))
{
_context.Warning($"Results file not found: {resultsPath}");
return failedTests;
}

try
{
var doc = System.Xml.Linq.XDocument.Load(resultsPath.FullPath);
var nodes = doc.XPathSelectElements("//test-case[@success='False']");

foreach (var node in nodes)
// XUnit v2 XML format uses @result='Fail' attribute
// Try both formats for compatibility
var failedNodes = doc.XPathSelectElements("//test[@result='Fail']")
.Concat(doc.XPathSelectElements("//test-case[@result='Fail']"))
.Concat(doc.XPathSelectElements("//test-case[@success='False']"));

foreach (var node in failedNodes)
{
var testName = node.Attribute("name")?.Value;
if (!string.IsNullOrEmpty(testName))
{
failedTests.Add(TrimTestMethod(testName));
var trimmedName = TrimTestMethod(testName);
if (!failedTests.Contains(trimmedName))
{
_context.Information($"Found failed test: {trimmedName}");
failedTests.Add(trimmedName);
}
}
}

_context.Information($"Total failed tests found: {failedTests.Count}");
}
catch (Exception ex)
{
_context.Warning($"Error parsing XUnit results: {ex.Message}");
_context.Warning($"Stack trace: {ex.StackTrace}");
}

return failedTests;
Expand Down Expand Up @@ -97,6 +113,78 @@ public class TestRetryHelper
}
return basePath;
}

/// <summary>
/// Displays a formatted summary table of test retry results
/// </summary>
/// <param name="testType">The type of tests (e.g., ".NET Framework Unit", ".NET Standard Integration")</param>
/// <param name="initialFailedTests">List of tests that failed initially</param>
/// <param name="stillFailedTests">List of tests that still failed after retry</param>
public void DisplayRetryResultsSummary(string testType, List<string> initialFailedTests, List<string> stillFailedTests)
{
var passedTests = initialFailedTests.Except(stillFailedTests).ToList();

if (passedTests.Any())
{
_context.Information("");
_context.Information("✓ Tests that PASSED on retry:");
foreach (var test in passedTests)
{
_context.Information($" • {test}");
}
}

if (stillFailedTests.Any())
{
_context.Information("");
_context.Warning("✗ Tests that FAILED after retry:");
foreach (var test in stillFailedTests)
{
_context.Warning($" • {test}");
}
}
else
{
_context.Information("");
_context.Information("✓ All retried tests passed!");
}

_context.Information("");
}

/// <summary>
/// Validates failed tests and throws an exception if any non-flaky tests failed.
/// Tests ending with "_Flaky" are ignored.
/// </summary>
/// <param name="stillFailedTests">List of tests that still failed after retry</param>
public void ValidateFlakyTests(List<string> stillFailedTests)
{
if (!stillFailedTests.Any())
{
return;
}

var nonFlakyFailedTests = stillFailedTests.Where(test => !test.EndsWith("_Flaky")).ToList();

if (nonFlakyFailedTests.Any())
{
_context.Error("");
_context.Error($"✗ {nonFlakyFailedTests.Count} non-flaky test(s) failed after retry:");
foreach (var test in nonFlakyFailedTests)
{
_context.Error($" • {test}");
}
_context.Error("");

throw new Exception($"{nonFlakyFailedTests.Count} test(s) failed after retry");
}
else
{
_context.Information("");
_context.Information($"ℹ All {stillFailedTests.Count} failed test(s) are marked as flaky (ending with '_Flaky')");
_context.Information("");
}
}
}

var testRetryHelper = new TestRetryHelper(Context);
85 changes: 57 additions & 28 deletions cake-build/tasks/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Task("_Clean")
.Does(() =>
{
Information("Cleaning build directories...");

// Clean the main solution which includes all core projects (NetFramework, NetStandard, iOS, Android, Tests, etc.)
CleanSolution();

// Clean custom output directories that are not part of standard project outputs
CleanDirectory(paths.TestResults);
CleanDirectory(paths.TestResults);
});

Task("_Restore_Main")
Expand All @@ -25,9 +25,9 @@ Task("_Version")
.Does(() =>
{
Information($"Setting version to {version}");

var assemblyInfoPath = paths.Src.CombineWithFilePath("CommonAssemblyInfo.cs");

CreateAssemblyInfo(assemblyInfoPath, new AssemblyInfoSettings
{
Company = "Ably",
Expand All @@ -43,37 +43,37 @@ Task("_NetFramework_Build")
.Does(() =>
{
Information("Building .NET Framework solution...");

var settings = buildConfig.ApplyStandardSettings(
new MSBuildSettings(),
configuration
);

settings = settings.WithTarget("Build");

MSBuild(paths.NetFrameworkSolution, settings);
});

Task("_NetStandard_Build")
.Does(() =>
{
Information("Building .NET Standard solution...");

var settings = new DotNetBuildSettings
{
Configuration = configuration,
NoRestore = true
};
var msbuildSettings = new DotNetMSBuildSettings();


if (!string.IsNullOrEmpty(defineConstants))
{
msbuildSettings = msbuildSettings.WithProperty("DefineConstants", defineConstants);
}

settings.MSBuildSettings = msbuildSettings;

DotNetBuild(paths.NetStandardSolution.FullPath, settings);
});

Expand All @@ -87,20 +87,20 @@ Task("_Xamarin_Build")
.Does(() =>
{
Information("Building Xamarin solution...");

if (!FileExists(paths.XamarinSolution))
{
Warning("Xamarin solution not found, skipping build");
return;
}

var settings = buildConfig.ApplyStandardSettings(
new MSBuildSettings(),
configuration
);

settings = settings.WithTarget("Build");

MSBuild(paths.XamarinSolution, settings);
});

Expand All @@ -109,32 +109,32 @@ Task("_Build_Ably_Unity_Dll")
.Does(() =>
{
Information("Merging Unity dependencies into IO.Ably.dll...");

var netStandard20BinPath = paths.Src
.Combine("IO.Ably.NETStandard20")
.Combine("bin/Release/netstandard2.0");

if (!DirectoryExists(netStandard20BinPath))
{
throw new Exception($"NETStandard2.0 bin directory not found: {netStandard20BinPath}. Please build the project first.");
}

var primaryDll = netStandard20BinPath.CombineWithFilePath("IO.Ably.dll");

if (!FileExists(primaryDll))
{
throw new Exception($"Primary DLL not found: {primaryDll}. Please build the IO.Ably.NETStandard20 project first.");
}

var newtonsoftDll = paths.Root
.Combine("lib/unity/AOT")
.CombineWithFilePath("Newtonsoft.Json.dll");

if (!FileExists(newtonsoftDll))
{
throw new Exception($"Newtonsoft.Json.dll not found at: {newtonsoftDll}");
}

var dllsToMerge = new[]
{
netStandard20BinPath.CombineWithFilePath("IO.Ably.DeltaCodec.dll"),
Expand All @@ -143,23 +143,47 @@ Task("_Build_Ably_Unity_Dll")
netStandard20BinPath.CombineWithFilePath("System.Threading.Tasks.Extensions.dll"),
newtonsoftDll
};

var unityOutputPath = paths.Root.Combine("unity/Assets/Ably/Plugins");
var outputDll = unityOutputPath.CombineWithFilePath("IO.Ably.dll");

// Delete existing output DLL if it exists
if (FileExists(outputDll))
{
DeleteFile(outputDll);
Information($"Deleted existing DLL: {outputDll}");
}

// Merge all dependencies into primary DLL in one go
ilRepackHelper.MergeDLLs(primaryDll, dllsToMerge, outputDll);

Information($"✓ Unity DLL created at: {outputDll}");
});

Task("_Format_Code")
.Description("Format C#, XML and other files")
.Does(() =>
{
Information("Formatting code with dotnet-format...");

// Using 'whitespace' mode for fast formatting without building the project
// This applies .editorconfig rules for whitespace, indentation, etc. without semantic analysis
// Much faster than default mode which requires compilation
var exitCode = StartProcess("dotnet", new ProcessSettings
{
Arguments = $"format {paths.MainSolution.FullPath} whitespace --no-restore"
});

if (exitCode == 0)
{
Information("✓ Code formatted successfully");
}
else
{
throw new Exception($"dotnet format failed with exit code {exitCode}");
}
});

///////////////////////////////////////////////////////////////////////////////
// PUBLIC TARGETS
///////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -190,3 +214,8 @@ Task("Build.Xamarin")
Task("Update.AblyUnity")
.Description("Update Ably DLLs inside unity project")
.IsDependentOn("_Build_Ably_Unity_Dll");

// Public task: Format code using dotnet-format
Task("Format.Code")
.Description("Format code using dotnet-format")
.IsDependentOn("_Format_Code");
Loading
Loading