12 Commits

Author SHA1 Message Date
Iain Sproat 251d6b7f2d chore(domains): update latest.speckle.dev to latest.speckle.systems (#8) 2024-07-18 16:58:16 +01:00
connorivy d20d3d022a Merge pull request #4 from specklesystems/function-memory
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
Update main.yml
2024-02-16 08:31:27 -06:00
Jedd Morgan b6e9dd9b5d Update main.yml 2024-02-15 15:41:41 +00:00
Claire Kuang fc5a7c680e Update AutomateFunction.cs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-07 14:26:55 +00:00
Claire Kuang 5569c0d4fd Update AutomateFunction.cs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-07 14:20:32 +00:00
Claire Kuang adc27ae5d3 adds error reporting
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-07 14:01:24 +00:00
Claire Kuang cacd6f03f5 Update AutomateFunction.cs 2024-02-02 02:20:26 +00:00
Claire Kuang e85e00bff2 Update Utils.cs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-02 02:06:55 +00:00
Claire Kuang 739befc3fa cleans and improves logic to handle no appid objects
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
also changes assumptions that in a single commit, multiple objects can have the same appid and same speckle id
2024-02-02 01:58:54 +00:00
Claire Kuang 5ed6ab2f69 attach warning test
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-01 17:25:12 +00:00
Claire Kuang 470b3d93de adds additional information to run failed log 2024-02-01 11:13:55 +00:00
Claire Kuang b9059e1b85 Update AutomateFunction.cs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-01 10:20:36 +00:00
10 changed files with 802 additions and 201 deletions
+1
View File
@@ -34,3 +34,4 @@ jobs:
speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }}
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
speckle_function_recommended_memory_mi: 8000
+6
View File
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpeckleAutomateDotnetExample", "SpeckleAutomateDotnetExample\SpeckleAutomateDotnetExample.csproj", "{E1DE5809-1111-4817-9B7D-7ADCA087FA1C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestAutomateFunction", "TestAutomateFunction\TestAutomateFunction.csproj", "{B74B115F-D553-4FD5-8E4C-7B13134217DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{E1DE5809-1111-4817-9B7D-7ADCA087FA1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1DE5809-1111-4817-9B7D-7ADCA087FA1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1DE5809-1111-4817-9B7D-7ADCA087FA1C}.Release|Any CPU.Build.0 = Release|Any CPU
{B74B115F-D553-4FD5-8E4C-7B13134217DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B74B115F-D553-4FD5-8E4C-7B13134217DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B74B115F-D553-4FD5-8E4C-7B13134217DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B74B115F-D553-4FD5-8E4C-7B13134217DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+228 -197
View File
@@ -1,17 +1,15 @@
using Objects;
using Speckle.Automate.Sdk;
using Speckle.Automate.Sdk.Schema;
using Speckle.Core.Api;
using Speckle.Core.Credentials;
using Speckle.Core.Models;
using Speckle.Core.Models.Extensions;
using Speckle.Core.Transports;
using SpeckleAutomateDotnetExample;
static class AutomateFunction
public static class AutomateFunction
{
public static string ADDED = "Added";
public static string MODIFIED = "Modified";
public static string DELETED = "Deleted";
public static string ADDED = "ADDED";
public static string MODIFIED = "MODIFIED";
public static string DELETED = "DELETED";
public static string UNCHANGED = "UNCHANGED";
public static async Task Run(
AutomationContext automationContext,
@@ -21,9 +19,10 @@ static class AutomateFunction
Console.WriteLine("Starting execution");
_ = typeof(ObjectsKit).Assembly; // INFO: Force objects kit to initialize
// get the test and release branch name
var testBranchName = automationContext.AutomationRunData.BranchName;
var releaseBranchName = testBranchName.Replace("/testing", "/release");
// get the testing and release branches
string testBranchName = automationContext.AutomationRunData.BranchName;
string releaseBranchName = functionInputs.DiffBranch;
Console.WriteLine($"Comparing {testBranchName} against {releaseBranchName}");
Branch? releaseBranch = await automationContext.SpeckleClient
.BranchGet(automationContext.AutomationRunData.ProjectId, releaseBranchName, 1)
.ConfigureAwait(false);
@@ -32,235 +31,267 @@ static class AutomateFunction
throw new Exception("Release branch was null");
}
// get the release branch latest commit
Commit releaseCommit = releaseBranch.commits.items.First();
if (releaseCommit is null)
{
throw new Exception("Release branch has no commits");
throw new Exception("Diff branch has no commits");
}
var tolerance = functionInputs.Tolerance;
Console.WriteLine($"Comparing {testBranchName} against {releaseBranchName}");
// get the test and release commits
// get the test and release commit base
Console.WriteLine("Receiving test version");
Base? testingCommitObject = await automationContext.ReceiveVersion();
Base testingCommitObject = await automationContext.ReceiveVersion();
Console.WriteLine("Received test version: " + testingCommitObject);
Console.WriteLine("Receiving release version");
ServerTransport serverTransport = new ServerTransport(
automationContext.SpeckleClient.Account,
automationContext.AutomationRunData.ProjectId
Base releaseCommitObject = await Utils.RecieveVersionAsync(
releaseCommit.id,
automationContext
);
Base? releaseCommitObject = await Operations
.Receive(
(
await automationContext.SpeckleClient
.CommitGet(automationContext.AutomationRunData.ProjectId, releaseCommit.id)
.ConfigureAwait(continueOnCapturedContext: false)
).referencedObject,
serverTransport,
new MemoryTransport()
)
.ConfigureAwait(continueOnCapturedContext: false);
if (releaseCommitObject == null)
{
throw new Exception("Commit root object was null");
}
Console.WriteLine("Received release version: " + releaseCommitObject);
// flatten both commits
IEnumerable<Base> releaseCommitObjects = releaseCommitObject.Flatten();
IEnumerable<Base> testCommitObjects = testingCommitObject.Flatten();
var releaseCommitObjectsDict = new Dictionary<string, Base>();
foreach (var releaseObject in releaseCommitObjects)
// Create dictionaries by appId (or speckle id if no appId exists) for the release and testing commits
// note that it is possible for multiple objects to have the same app id
// (eg mapped schema objects have the same id as their parent geometry)
// and it is also possible for multiple objects (with no app id) to have the same speckle id
// (eg the same mesh sent from gh multiple times)
Dictionary<string, List<Base>> releaseCommitAppIdDict = new();
Dictionary<string, List<Base>> releaseCommitSpeckleIdDict = new();
Dictionary<string, List<Base>> testingCommitAppIdDict = new();
Dictionary<string, List<Base>> testingCommitSpeckleIdDict = new();
Utils.CreateDictionaryFromBaseById(
releaseCommitObject,
out releaseCommitAppIdDict,
out releaseCommitSpeckleIdDict,
out int releaseObjectCount
);
Console.WriteLine(
$"Found {releaseObjectCount} objects in RELEASE with {releaseCommitAppIdDict.Count} unique applicationIds and {releaseCommitSpeckleIdDict.Count} unique speckle ids (for objects with no application id)."
);
Utils.CreateDictionaryFromBaseById(
testingCommitObject,
out testingCommitAppIdDict,
out testingCommitSpeckleIdDict,
out int testingObjectCount
);
Console.WriteLine(
$"Found {testingObjectCount} objects in TESTING with {testingCommitAppIdDict.Count} unique applicationIds and {testingCommitSpeckleIdDict.Count} unique speckle ids (for objects with no application id)."
);
// COMPARE COMMIT OBJECTS WITH APPLICATION IDS
// and store in hash lists where each object is (id, appId, type), and for modified, and additional string of property changes.
HashSet<Tuple<string, string?, string>> deletedAppIdObjects = new();
HashSet<Tuple<string, string?, string>> unchangedAppIdObjects = new();
HashSet<Tuple<string, string?, string>> addedAppIdObjects = new();
HashSet<Tuple<string, string?, string, string>> modifiedAppIdObjects = new();
// first find deleted objects in the testing commit and remove their keys from the release commit dict
foreach (string releaseAppId in releaseCommitAppIdDict.Keys)
{
if (
releaseObject.applicationId != null
&& !releaseCommitObjectsDict.ContainsKey(releaseObject.applicationId)
)
if (!testingCommitAppIdDict.ContainsKey(releaseAppId))
{
releaseCommitObjectsDict.Add(releaseObject.applicationId, releaseObject);
releaseCommitAppIdDict[releaseAppId].ForEach(
o =>
deletedAppIdObjects.Add(
new Tuple<string, string?, string>(o.id, releaseAppId, o.speckle_type)
)
);
releaseCommitAppIdDict.Remove(releaseAppId);
}
}
Console.WriteLine(
$"Found {releaseCommitObjects.Count()} objects in release version"
);
Console.WriteLine($"Found {testCommitObjects.Count()} objects in release version");
// compare objects
int unchangedCount = 0;
var addedList = new List<Tuple<string, string>>();
var modifiedList = new List<Tuple<string, string, string>>();
foreach (Base testObject in testCommitObjects)
// then find unchanged, added, and modified objects by iterating through testing commit app ids
foreach (string testingAppId in testingCommitAppIdDict.Keys)
{
if (
testObject.applicationId != null
&& releaseCommitObjectsDict.ContainsKey(testObject.applicationId)
)
List<Base> testObjects = testingCommitAppIdDict[testingAppId];
// test for added objects
if (!releaseCommitAppIdDict.ContainsKey(testingAppId))
{
Base releaseObject = releaseCommitObjectsDict[testObject.applicationId];
testObjects.ForEach(
o =>
addedAppIdObjects.Add(
new Tuple<string, string?, string>(o.id, testingAppId, o.speckle_type)
)
);
}
else
{
List<Base> releaseObjects = releaseCommitAppIdDict[testingAppId];
// if these have the same hash, no properties have changed
if (testObject.id == releaseObject.id)
// test for unchanged objects
// by filtering the testing and release objects with matching speckle ids
Utils
.FilterListsBySpeckleIdMatch(testObjects, releaseObjects)
.ForEach(o => unchangedAppIdObjects.Add(o));
// for remaining objects, determine deleted objects
// and then compare them in order (assume modified) and handle leftovers (added)
// this is imperfect, as there's a chance we are not comparing the correct objects.
if (releaseObjects.Count > testObjects.Count)
{
unchangedCount++;
for (int i = releaseObjects.Count - 1; i >= testObjects.Count; i--)
{
deletedAppIdObjects.Add(
new Tuple<string, string?, string>(
releaseObjects[i].id,
testingAppId,
releaseObjects[i].speckle_type
)
);
releaseObjects.RemoveAt(i);
}
}
// if ids are different, find property differences
else
for (int i = 0; i < testObjects.Count; i++)
{
var diffDictionary = new Dictionary<string, string>();
Dictionary<string, object?> releaseObjectPropDict =
releaseObject.GetMembers();
Dictionary<string, object?> testObjectPropDict = testObject.GetMembers();
foreach (var entry in testObjectPropDict)
{
if (releaseObjectPropDict.ContainsKey(entry.Key))
{
try
{
bool changed = !Equals(entry.Value, releaseObjectPropDict[entry.Key]);
if (changed)
{
string diff =
$"Property ({entry.Key}) changed from ({releaseObjectPropDict[entry.Key]}) to ({entry.Value})";
if (!diffDictionary.ContainsKey(entry.Key))
{
diffDictionary.Add(entry.Key, diff);
}
}
}
catch { }
releaseObjectPropDict.Remove(entry.Key);
}
else
{
if (!diffDictionary.ContainsKey(entry.Key))
{
diffDictionary.Add(entry.Key, ADDED);
}
}
}
Base testObject = testObjects[i];
// check if there are any props left on the release object - these were missing in the test object
foreach (var entry in releaseObjectPropDict)
if (i < releaseObjectCount)
{
if (!diffDictionary.ContainsKey(entry.Key))
{
diffDictionary.Add(entry.Key, DELETED);
}
}
Base releaseObject = releaseObjects[i];
// add the diff dict info to the object
if (diffDictionary.Count > 0)
{
// compare object properties to determine changes
List<string> addedProps = new();
List<string> deletedProps = new();
List<Tuple<string, string>> modifiedProps = new();
Utils.CompareBaseProperties(
testObject,
releaseObject,
out addedProps,
out deletedProps,
out modifiedProps
);
var sb = new System.Text.StringBuilder();
foreach (var entry in diffDictionary)
{
sb.AppendLine($"{entry.Key}: {entry.Value}. ");
}
modifiedList.Add(
new Tuple<string, string, string>(
addedProps.ForEach(s => sb.AppendLine($"{ADDED} prop ({s})."));
deletedProps.ForEach(s => sb.AppendLine($"{DELETED} prop ({s})."));
modifiedProps.ForEach(
t => sb.AppendLine($"{MODIFIED} ({t.Item2}) of prop ({t.Item1})")
);
modifiedAppIdObjects.Add(
new Tuple<string, string?, string, string>(
testObject.id,
testObject.applicationId,
testObject.speckle_type,
sb.ToString()
)
);
//automationContext.AttachWarningToObjects( MODIFIED, new List<string>() { testObject.id }, sb.ToString());
}
// remaining test objects are considered added
else
{
addedAppIdObjects.Add(
new Tuple<string, string?, string>(
testObject.id,
testingAppId,
testObject.speckle_type
)
);
}
}
releaseCommitObjectsDict.Remove(testObject.applicationId);
}
else
{
// we're skipping objects without an applicationId for now, since we're doing so in the release commit
if (!string.IsNullOrEmpty(testObject.applicationId))
{
//automationContext.AttachInfoToObjects(ADDED, new List<string>() { testObject.id });
addedList.Add(
new Tuple<string, string>(testObject.id, testObject.speckle_type)
);
}
}
}
// if there are any remaining release commit objects, this indicates missing objects in the test run.
var deletedList = new List<Tuple<string, string>>();
foreach (var entry in releaseCommitObjectsDict)
{
deletedList.Add(new Tuple<string, string>(entry.Key, entry.Value.speckle_type));
}
// COMPARE COMMIT OBJECTS WITHOUT APPLICATION IDS USING SPECKLE IDS
// since we only have 1 parameter of comparison, we can rule out any matching speckle ids as unchanged.
// for all other objects, mark as modified, and indicate quantity of any added or deleted objects
// store modified in hash lists of (id, type)
HashSet<Tuple<string, string?, string>> unchangedSpeckleIdObjects = new();
HashSet<Tuple<string, string>> changedSpeckleIdObjects = new();
// mark run failed if there are any added, modified, or deleted objects and report
if (addedList.Count + deletedList.Count + modifiedList.Count > 0)
{
automationContext.MarkRunFailed(
$"Run failed due to {addedList.Count} ADDED, {modifiedList.Count} MODIFIED, and {deletedList.Count} DELETED objects compared to the release commit."
);
// first filter out matching speckle ids
List<Base> flattenedTestingSpeckleIdDict = testingCommitSpeckleIdDict.Values
.SelectMany(o => o)
.ToList();
List<Base> flattenedReleaseSpeckleIdDict = releaseCommitSpeckleIdDict.Values
.SelectMany(o => o)
.ToList();
Utils
.FilterListsBySpeckleIdMatch(
flattenedTestingSpeckleIdDict,
flattenedReleaseSpeckleIdDict
)
.ForEach(o => unchangedSpeckleIdObjects.Add(o));
addedList.ForEach(
o => Console.WriteLine($"ADDED object: id( {o.Item1} ), type( {o.Item2} )")
);
deletedList.ForEach(
o => Console.WriteLine($"DELETED object: id( {o.Item1} ), type( {o.Item2} )")
);
modifiedList.ForEach(
o =>
Console.WriteLine(
$"MODIFIED object: id( {o.Item1} ), type( {o.Item2} ), changed props:( {o.Item3} )"
)
);
// then store all remaining testing objects as changed
flattenedTestingSpeckleIdDict.ForEach(
o => changedSpeckleIdObjects.Add(new Tuple<string, string>(o.id, o.speckle_type))
);
// calculate count difference
int speckleIdObjectCountDifference =
flattenedTestingSpeckleIdDict.Count - flattenedReleaseSpeckleIdDict.Count;
// REPORT ALL DIFF RESULTS FOR APP IDS AND SPECKLE IDS
// mark run succeeded if there are no added, modified, or deleted app id objects, and no changed speckle id objects
// mark run failed otherwise
if (
addedAppIdObjects.Count
+ deletedAppIdObjects.Count
+ modifiedAppIdObjects.Count
+ changedSpeckleIdObjects.Count
== 0
)
{
automationContext.MarkRunSuccess($"Run passed with no changes to objects.");
}
else
{
automationContext.MarkRunSuccess(
$"Run passed with {unchangedCount} unchanged objects."
automationContext.MarkRunFailed(
$"Run failed due to: {addedAppIdObjects.Count} {ADDED}, {modifiedAppIdObjects.Count} {MODIFIED}, and {deletedAppIdObjects.Count} {DELETED} objects WITH APP IDS, and {(speckleIdObjectCountDifference > 0 ? $"{speckleIdObjectCountDifference} {ADDED}" : $"{Math.Abs(speckleIdObjectCountDifference)} {DELETED}")} and {changedSpeckleIdObjects.Count} CHANGED objects WITHOUT APP IDS compared to the release commit. "
);
}
}
public static bool Equals<T>(T a, T b)
{
switch (a)
{
case Base o:
return b is Base bBase ? o.id == bBase.id : false;
case List<object> aList:
if (b is List<object> bList && aList.Count == bList.Count)
{
for (int i = 0; i < aList.Count; i++)
{
if (!Equals(aList[i], bList[i]))
{
return false;
}
}
return true;
}
return false;
case Dictionary<string, object> aDictionary:
if (
b is Dictionary<string, object> bDictionary
&& aDictionary.Count == bDictionary.Count
)
{
foreach (var entry in aDictionary)
{
if (
!bDictionary.ContainsKey(entry.Key)
|| !Equals(entry.Value, bDictionary[entry.Key])
)
{
return false;
}
}
return true;
}
return false;
default:
return EqualityComparer<T>.Default.Equals(a, b);
foreach (var added in addedAppIdObjects)
{
Console.WriteLine(
$"{ADDED} {added.Item3} object: id( {added.Item1} ), appId: {added.Item2}"
);
}
if (addedAppIdObjects.Count > 0)
{
automationContext.AttachErrorToObjects(
"ADDED",
addedAppIdObjects.Select(o => o.Item1),
"added objects with an application Id"
);
}
foreach (var deleted in deletedAppIdObjects)
{
Console.WriteLine(
$"{DELETED} {deleted.Item3} object: id( {deleted.Item1} ), appId: {deleted.Item2}"
);
}
foreach (var modified in modifiedAppIdObjects)
{
Console.WriteLine(
$"{MODIFIED} {modified.Item3} object: id( {modified.Item1} ), appId: {modified.Item2}, category: {modified.Item4}"
);
}
if (modifiedAppIdObjects.Count > 0)
{
automationContext.AttachErrorToObjects(
"MODIFIED",
modifiedAppIdObjects.Select(o => o.Item1),
"modified objects with an application Id"
);
}
foreach (var changed in changedSpeckleIdObjects)
{
Console.WriteLine($"CHANGED {changed.Item2} object: id( {changed.Item1} )");
}
if (changedSpeckleIdObjects.Count > 0)
{
automationContext.AttachErrorToObjects(
"CHANGED",
changedSpeckleIdObjects.Select(o => o.Item1),
"changed objects with no application Id"
);
}
}
}
}
@@ -5,10 +5,11 @@ using System.ComponentModel.DataAnnotations;
/// </summary>
/// This class is used to generate a JSON Schema to ensure that the user provided values
/// are valid and match the required schema.
struct FunctionInputs
public struct FunctionInputs
{
/// <summary>
/// The name of the branch to compare against. This should be the full branch path.
/// </summary>
[Required]
public double Tolerance;
//public string Exclusions;
public string DiffBranch;
}
+275
View File
@@ -0,0 +1,275 @@
using Speckle.Automate.Sdk;
using Speckle.Automate.Sdk.Schema;
using Speckle.Core.Api;
using Speckle.Core.Models;
using Speckle.Core.Models.Extensions;
using Speckle.Core.Transports;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpeckleAutomateDotnetExample
{
internal class Utils
{
/// <summary>
/// Receives a commit object from the same project and account as the <paramref name="context"/>
/// </summary>
/// <param name="commitId"> The id of the commit to receive</param>
/// <param name="context">The Automation context</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<Base> RecieveVersionAsync(
string commitId,
AutomationContext context
)
{
ServerTransport serverTransport = new ServerTransport(
context.SpeckleClient.Account,
context.AutomationRunData.ProjectId
);
Base? receivedCommitObject = await Operations
.Receive(
(
await context.SpeckleClient
.CommitGet(context.AutomationRunData.ProjectId, commitId)
.ConfigureAwait(continueOnCapturedContext: false)
).referencedObject,
serverTransport,
new MemoryTransport()
)
.ConfigureAwait(continueOnCapturedContext: false);
if (receivedCommitObject == null)
{
throw new Exception("Commit root object was null");
}
return receivedCommitObject;
}
/// <summary>
/// Creates Dictionaries from a commit Base, using applicationId if available or speckle id if no applicationId exists.
/// </summary>
/// <param name="commit">The commit Base to create dictionaries from</param>
/// <param name="appIdDict">The dictionary of commit objects sorted by applicationId</param>
/// <param name="speckleIdDict">The dictionary of commit objects without applicationIds, sorted by speckle id</param>
public static void CreateDictionaryFromBaseById(
Base commit,
out Dictionary<string, List<Base>> appIdDict,
out Dictionary<string, List<Base>> speckleIdDict,
out int objectCount
)
{
IEnumerable<Base> commitObjects = commit.Flatten();
objectCount = commitObjects.Count();
appIdDict = new Dictionary<string, List<Base>>();
speckleIdDict = new Dictionary<string, List<Base>>();
foreach (var commitObject in commitObjects)
{
if (!string.IsNullOrWhiteSpace(commitObject.applicationId))
{
if (appIdDict.ContainsKey(commitObject.applicationId))
{
appIdDict[commitObject.applicationId].Add(commitObject);
}
else
{
appIdDict.Add(
commitObject.applicationId,
new List<Base>() { commitObject }
);
}
}
else if (!string.IsNullOrWhiteSpace(commitObject.id))
{
if (speckleIdDict.ContainsKey(commitObject.id))
{
speckleIdDict[commitObject.id].Add(commitObject);
}
else
{
speckleIdDict.Add(commitObject.id, new List<Base>() { commitObject });
}
}
}
}
/// <summary>
/// Filters two lists by removing all objects from <paramref name="setA"/> with a matching speckle id in <paramref name="setB"/>
/// </summary>
/// <param name="setA">The list to filter</param>
/// <param name="setB">The list to filter against</param>
/// <returns>A list of matches found in <paramref name="setA"/></returns>
public static List<Tuple<string, string?, string>> FilterListsBySpeckleIdMatch(
List<Base> setA,
List<Base> setB
)
{
var matches = new List<Tuple<string, string?, string>>();
for (int i = setA.Count - 1; i >= 0; i--)
{
var testObject = setA[i];
for (int j = setB.Count - 1; j >= 0; j--)
{
var releaseObject = setB[j];
// if a match was found, remove from both lists and add to matches
if (testObject.id == releaseObject.id)
{
matches.Add(
new Tuple<string, string?, string>(
testObject.id,
testObject.applicationId,
testObject.speckle_type
)
);
setA.RemoveAt(i);
setB.RemoveAt(j);
}
}
}
return matches;
}
/// <summary>
/// Compares the properties of <paramref name="a"/> against <paramref name="b"/>
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="addedProps">Properties on <paramref name="a"/> that do not exist on <paramref name="b"/></param>
/// <param name="deletedProps">Properties on <paramref name="b"/> that do not exist on <paramref name="a"/></param>
/// <param name="modifiedProps">Properties on <paramref name="a"/> that have a different value on <paramref name="b"/>. The second string is the category of modification (eg count/primitive/base)</param>
public static void CompareBaseProperties(
Base a,
Base b,
out List<string> addedProps,
out List<string> deletedProps,
out List<Tuple<string, string>> modifiedProps
)
{
addedProps = new List<string>();
deletedProps = new List<string>();
modifiedProps = new List<Tuple<string, string>>();
Dictionary<string, object?> bObjectPropDict = b.GetMembers();
Dictionary<string, object?> aObjectPropDict = a.GetMembers();
foreach (KeyValuePair<string, object?> entry in aObjectPropDict)
{
if (bObjectPropDict.ContainsKey(entry.Key))
{
bool changed = !IsEqual(
entry.Value,
bObjectPropDict[entry.Key],
out string category
);
if (changed)
{
modifiedProps.Add(new Tuple<string, string>(entry.Key, category));
}
bObjectPropDict.Remove(entry.Key);
}
else
{
addedProps.Add(entry.Key);
}
}
// check if there are any props left on the b - these were missing in the a
foreach (var entry in bObjectPropDict)
{
deletedProps.Add(entry.Key);
}
}
/// <summary>
/// Determines equality between two objects
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="category">The category of the modification, if objects are not equal</param>
/// <returns></returns>
public static bool IsEqual<T>(T a, T b, out string category)
{
category = "Value";
switch (a)
{
case Base aBase:
if (b is Base bBase && bBase.speckle_type == aBase.speckle_type)
{
return aBase.id == bBase.id;
}
else
{
category = "Type";
return false;
}
case List<object> aList:
if (b is List<object> bList)
{
if (aList.Count != bList.Count)
{
category = "Count";
return false;
}
for (int i = 0; i < aList.Count; i++)
{
if (!Equals(aList[i], bList[i]))
{
return false;
}
}
return true;
}
else
{
category = "Type";
return false;
}
case Dictionary<string, object> aDictionary:
if (b is Dictionary<string, object> bDictionary)
{
if (aDictionary.Count != bDictionary.Count)
{
category = "Count";
return false;
}
foreach (var entry in aDictionary)
{
if (
!bDictionary.ContainsKey(entry.Key)
|| !Equals(entry.Value, bDictionary[entry.Key])
)
{
return false;
}
}
return true;
}
else
{
category = "Type";
return false;
}
default:
try
{
return EqualityComparer<T>.Default.Equals(a, b);
}
catch
{
return false;
}
}
}
}
}
@@ -0,0 +1,150 @@
# nullable enable
namespace TestAutomateFunction;
using Speckle.Automate.Sdk.Schema;
using Speckle.Automate.Sdk;
using Speckle.Core.Api;
using Speckle.Core.Credentials;
using Speckle.Core.Models;
using Speckle.Core.Transports;
using Utils = TestAutomateUtils;
[TestFixture]
public sealed class AutomationContextTest : IDisposable
{
// Regression testing function project id
public string projectId = "6ada180f0c";
// e2e release branch and model id
public string releaseBranchName = "e2e/release";
public string releaseModelId = "e4c8cc3802";
// e2e testing branch and model id
public string testingBranchName = "e2e/testing";
public string testingModelId = "c374f0aade";
private async Task<AutomationRunData> AutomationRunData(
Base testObject,
Base releaseObject
)
{
// send the release commit to the release branch
string releaseObjId = await Operations.Send(
releaseObject,
new List<ITransport> { new ServerTransport(client.Account, projectId) }
);
string releaseVersionId = await client.CommitCreate(
new()
{
streamId = projectId,
objectId = releaseObjId,
branchName = releaseBranchName
}
);
// send the test object to the test branch
string testObjId = await Operations.Send(
testObject,
new List<ITransport> { new ServerTransport(client.Account, projectId) }
);
string testingVersionId = await client.CommitCreate(
new()
{
streamId = projectId,
objectId = testObjId,
branchName = testingBranchName
}
);
// create a new automation on the testing branch
var automationName = TestAutomateUtils.RandomString(10);
var automationId = TestAutomateUtils.RandomString(10);
var automationRevisionId = TestAutomateUtils.RandomString(10);
await TestAutomateUtils.RegisterNewAutomation(
projectId,
testingModelId,
client,
automationId,
automationName,
automationRevisionId
);
var automationRunId = TestAutomateUtils.RandomString(10);
var functionId = TestAutomateUtils.RandomString(10);
var functionName = "Automation name " + TestAutomateUtils.RandomString(10);
var functionRelease = TestAutomateUtils.RandomString(10);
return new AutomationRunData
{
ProjectId = projectId,
ModelId = testingModelId,
BranchName = testingBranchName,
VersionId = testingVersionId,
SpeckleServerUrl = client.ServerUrl,
AutomationId = automationId,
AutomationRevisionId = automationRevisionId,
AutomationRunId = automationRunId,
FunctionId = functionId,
FunctionName = functionName,
FunctionRelease = functionRelease,
};
}
private Client client;
private Account account;
private string GetSpeckleToken()
{
var envVarName = "SPECKLE_TOKEN";
Environment.SetEnvironmentVariable(envVarName, "");
var token = Environment.GetEnvironmentVariable(envVarName);
if (token is null)
{
throw new Exception(
$"Cannot run tests without a {envVarName} environment variable"
);
}
return token;
}
private string GetSpeckleServerUrl() =>
Environment.GetEnvironmentVariable("SPECKLE_SERVER_URL")
?? "https://latest.speckle.systems";
[OneTimeSetUp]
public void Setup()
{
account = new Account
{
token = GetSpeckleToken(),
serverInfo = new ServerInfo { url = GetSpeckleServerUrl() }
};
client = new Client(account);
}
[Test]
public async Task TestFunctionRun()
{
var automationRunData = await AutomationRunData(
TestAutomateUtils.TestObject(),
TestAutomateUtils.ReleaseObject()
);
var automationContext = await AutomationRunner.RunFunction(
AutomateFunction.Run,
automationRunData,
account.token,
new FunctionInputs { DiffBranch = releaseBranchName }
);
Assert.That(automationContext.RunStatus, Is.EqualTo("FAILED"));
}
public void Dispose()
{
client.Dispose();
}
}
@@ -0,0 +1,11 @@
{
"profiles": {
"TestAutomateFunction": {
"commandName": "Project",
"environmentVariables": {
"SPECKLE_TOKEN": "<YOUR TOKEN HERE>",
"SPECKLE_SERVER_URL": "https://latest.speckle.systems/"
}
}
}
}
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>0b5cf4dd-cfa3-4982-9722-ca6c6672b8dd</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="Speckle.Automate.Sdk" Version="2.18.0-fileInput" />
<PackageReference Include="Speckle.Objects" Version="2.18.0-fileInput" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SpeckleAutomateDotnetExample\SpeckleAutomateDotnetExample.csproj" />
</ItemGroup>
</Project>
+98
View File
@@ -0,0 +1,98 @@
using System.Diagnostics.CodeAnalysis;
using GraphQL;
using Speckle.Core.Api;
using Speckle.Core.Models;
namespace TestAutomateFunction;
public static class TestAutomateUtils
{
[SuppressMessage("Security", "CA5394:Do not use insecure randomness")]
public static string RandomString(int length)
{
Random rand = new();
const string pool = "abcdefghijklmnopqrstuvwxyz0123456789";
var chars = Enumerable
.Range(0, length)
.Select(_ => pool[rand.Next(0, pool.Length)]);
return new string(chars.ToArray());
}
/// <summary>
/// The test object to compare against the release object
/// </summary>
/// <returns></returns>
/// <remarks>Contains 3 children objects to capture the Added, Deleted, Modified, and Unchanged categories</remarks>
public static Base TestObject()
{
Base unchanged = new() { ["unchangedProp"] = true };
Base added = new() { ["addedProp"] = "added" };
Base modified = new() { ["modifiedProp"] = 1 };
Base testObject = new();
testObject["unchanged"] = unchanged;
testObject["added"] = added;
testObject["modified"] = modified;
return testObject;
}
/// <summary>
/// The release object to compare the test object against
/// </summary>
/// <returns>Contains 3 children objects to capture the Added, Deleted, Modified, and Unchanged categories</returns>
public static Base ReleaseObject()
{
Base unchanged = new() { ["unchangedProp"] = true };
Base deleted = new() { ["deletedProp"] = "deleted" };
Base modified = new() { ["modifiedProp"] = 0.5 };
Base releaseObject = new();
releaseObject["unchanged"] = unchanged;
releaseObject["deleted"] = deleted;
releaseObject["modified"] = modified;
return releaseObject;
}
public static async Task RegisterNewAutomation(
string projectId,
string modelId,
Client speckleClient,
string automationId,
string automationName,
string automationRevisionId
)
{
GraphQLRequest query =
new(
query: """
mutation CreateAutomation(
$projectId: String!
$modelId: String!
$automationName: String!
$automationId: String!
$automationRevisionId: String!
) {
automationMutations {
create(
input: {
projectId: $projectId
modelId: $modelId
automationName: $automationName
automationId: $automationId
automationRevisionId: $automationRevisionId
}
)
}
}
""",
variables: new
{
projectId,
modelId,
automationName,
automationId,
automationRevisionId,
}
);
await speckleClient.ExecuteGraphQLRequest<object>(query);
}
}
+1
View File
@@ -0,0 +1 @@
global using NUnit.Framework;