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
132 changes: 132 additions & 0 deletions .ci/code/Versioning_Test/Helpers/TopLevelFieldFromJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* This file is part of the Buildings and Habitats object Model (BHoM)
* Copyright (c) 2015 - 2026, the respective contributors. All rights reserved.
*
* Each contributor holds copyright over their respective contributions.
* The project versioning (Git) records all such contribution source information.
*
*
* The BHoM is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3.0 of the License, or
* (at your option) any later version.
*
* The BHoM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this code. If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
*/


using System.Text;

namespace BH.Test.Versioning
{
public static partial class Helpers
{
/*************************************/
/**** Public Methods ****/
/*************************************/

// Reads a top-level field from a serialised record. Depth-aware so an identically named
// field on a nested fragment is not mistaken for the record's own.
public static string TopLevelFieldFromJson(string json, string field)
{
if (string.IsNullOrEmpty(json) || string.IsNullOrEmpty(field))
return null;

int depth = 0;
bool inString = false;
bool escaped = false;
int stringStart = -1;

for (int i = 0; i < json.Length; i++)
{
char c = json[i];

if (inString)
{
if (escaped)
escaped = false;
else if (c == '\\')
escaped = true;
else if (c == '"')
{
inString = false;

// A string is a key only when a colon follows it; a value never is.
bool isWantedName = i - stringStart - 1 == field.Length
&& string.CompareOrdinal(json, stringStart + 1, field, 0, field.Length) == 0;

if (depth == 1 && isWantedName)
{
int j = i + 1;
while (j < json.Length && (json[j] == ' ' || json[j] == '\t'))
j++;

if (j < json.Length && json[j] == ':')
return StringValueAt(json, j + 1);
}
}

continue;
}

if (c == '"')
{
inString = true;
escaped = false;
stringStart = i;
}
else if (c == '{' || c == '[')
depth++;
else if (c == '}' || c == ']')
depth--;
}

return null;
}

/*************************************/
/**** Private Methods ****/
/*************************************/

private static string StringValueAt(string json, int from)
{
int i = from;
while (i < json.Length && (json[i] == ' ' || json[i] == '\t'))
i++;

// Present but not a string. Returning null leaves the caller on its existing path.
if (i >= json.Length || json[i] != '"')
return null;

StringBuilder value = new StringBuilder();
bool escaped = false;

for (i++; i < json.Length; i++)
{
char c = json[i];

if (escaped)
{
value.Append(c);
escaped = false;
}
else if (c == '\\')
escaped = true;
else if (c == '"')
return value.ToString();
else
value.Append(c);
}

return null;
}

/*************************************/
}
}
55 changes: 47 additions & 8 deletions .ci/code/Versioning_Test/Verify/FromJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +245,53 @@ public static TestResult FromJsonItem(string json, bool isMethod)

if (message == "")
return Engine.Test.Create.PassResult(description);
else
return new TestResult
{
Description = description,
Status = TestStatus.Error,
Message = message,
Information = Engine.Base.Query.CurrentEvents().Select(x => x.ToEventMessage()).ToList<ITestInformation>()
};

List<ITestInformation> information = Engine.Base.Query.CurrentEvents().Select(x => x.ToEventMessage()).ToList<ITestInformation>();

ITestInformation provenance = ProvenanceEvent(json, isMethod);
if (provenance != null)
information.Add(provenance);

return new TestResult
{
Description = description,
Status = TestStatus.Error,
Message = message,
Information = information
};
}

/*************************************/

// Surfaces the record's declaring assembly for the checker, which reads this TestResult
// and never the dataset file. The wording is a contract with CI_Toolkit's VersioningRunner:
//
// Object <FullTypeName> declared in "<AssemblyName>"
//
// The quoted part holds the assembly alone because a closed generic type name contains
// commas, so a comma-delimited form cannot represent one.
private static ITestInformation ProvenanceEvent(string json, bool isMethod)
{
// A method record already carries its declaring assembly, and its top-level type is
// System.Reflection.MethodBase, so emitting this for one would state something untrue.
if (isMethod)
return null;

string declaringAssembly = Helpers.TopLevelFieldFromJson(json, "_asm");
if (string.IsNullOrWhiteSpace(declaringAssembly))
return null;

string declaringType = Helpers.TopLevelFieldFromJson(json, "_t");
if (string.IsNullOrWhiteSpace(declaringType))
return null;

return new EventMessage
{
// Not a failure. Note this does not hide it from FullMessage, which defaults to
// minSeverity Pass.
Status = TestStatus.Pass,
Message = $"Object {declaringType} declared in \"{declaringAssembly}\""
};
}

/*************************************/
Expand Down
Loading