using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using Json.Schema;
using YamlDotNet.RepresentationModel;
namespace Tests.Discovery;
///
/// Captured output from real machines. Reading these rather than the host is what
/// lets one set of tests run unchanged on Linux, macOS and Windows.
///
public static class Fixture {
// JsonSchema.Net keeps a process-wide registry keyed on $id, so loading the same
// schema from two test classes at once races. Load each one exactly once.
private static readonly ConcurrentDictionary> _schemas = new();
public static string Read(string name) =>
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", name));
///
/// Asserts a discovery document satisfies the published RackPeek schema, so the
/// collectors cannot drift away from the contract the rest of the world imports.
///
public static void AssertConformsToSchema(string yaml, int version = 4) {
JsonSchema schema = _schemas.GetOrAdd(version, v => new Lazy(() =>
JsonSchema.FromText(
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "schemas", $"schema.v{v}.json"))),
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
EvaluationResults results = schema.Evaluate(
ToJson(yaml),
new EvaluationOptions { OutputFormat = OutputFormat.Hierarchical });
if (results.IsValid)
return;
var errors = new List();
Collect(results, errors);
Assert.Fail($"Discovery output does not match schema v{version}:{Environment.NewLine}"
+ string.Join(Environment.NewLine, errors.Distinct())
+ Environment.NewLine + Environment.NewLine + yaml);
}
private static void Collect(EvaluationResults node, List errors) {
if (node.Errors != null)
foreach (KeyValuePair error in node.Errors)
errors.Add($"{node.InstanceLocation}: {error.Value}");
if (node.Details != null)
foreach (EvaluationResults child in node.Details)
Collect(child, errors);
}
private static JsonElement ToJson(string yaml) {
var stream = new YamlStream();
stream.Load(new StringReader(yaml));
using var document = JsonDocument.Parse(Convert(stream.Documents[0].RootNode));
return document.RootElement.Clone();
}
private static string Convert(YamlNode node) {
switch (node) {
case YamlScalarNode scalar:
if (scalar.Style is YamlDotNet.Core.ScalarStyle.SingleQuoted
or YamlDotNet.Core.ScalarStyle.DoubleQuoted)
return JsonSerializer.Serialize(scalar.Value);
if (int.TryParse(scalar.Value, out var i))
return i.ToString();
if (double.TryParse(scalar.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
return d.ToString(CultureInfo.InvariantCulture);
if (bool.TryParse(scalar.Value, out var b))
return b.ToString().ToLowerInvariant();
return JsonSerializer.Serialize(scalar.Value);
case YamlSequenceNode sequence:
return "[" + string.Join(",", sequence.Children.Select(Convert)) + "]";
case YamlMappingNode mapping:
return "{" + string.Join(",", mapping.Children.Select(kvp =>
JsonSerializer.Serialize(((YamlScalarNode)kvp.Key).Value) + ":" + Convert(kvp.Value))) + "}";
default:
return "null";
}
}
}