Fixture.cs 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. using System.Collections.Concurrent;
  2. using System.Globalization;
  3. using System.Text.Json;
  4. using Json.Schema;
  5. using YamlDotNet.RepresentationModel;
  6. namespace Tests.Discovery;
  7. /// <summary>
  8. /// Captured output from real machines. Reading these rather than the host is what
  9. /// lets one set of tests run unchanged on Linux, macOS and Windows.
  10. /// </summary>
  11. public static class Fixture {
  12. // JsonSchema.Net keeps a process-wide registry keyed on $id, so loading the same
  13. // schema from two test classes at once races. Load each one exactly once.
  14. private static readonly ConcurrentDictionary<int, Lazy<JsonSchema>> _schemas = new();
  15. public static string Read(string name) =>
  16. File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", name));
  17. /// <summary>
  18. /// Asserts a discovery document satisfies the published RackPeek schema, so the
  19. /// collectors cannot drift away from the contract the rest of the world imports.
  20. /// </summary>
  21. public static void AssertConformsToSchema(string yaml, int version = 4) {
  22. JsonSchema schema = _schemas.GetOrAdd(version, v => new Lazy<JsonSchema>(() =>
  23. JsonSchema.FromText(
  24. File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "schemas", $"schema.v{v}.json"))),
  25. LazyThreadSafetyMode.ExecutionAndPublication)).Value;
  26. EvaluationResults results = schema.Evaluate(
  27. ToJson(yaml),
  28. new EvaluationOptions { OutputFormat = OutputFormat.Hierarchical });
  29. if (results.IsValid)
  30. return;
  31. var errors = new List<string>();
  32. Collect(results, errors);
  33. Assert.Fail($"Discovery output does not match schema v{version}:{Environment.NewLine}"
  34. + string.Join(Environment.NewLine, errors.Distinct())
  35. + Environment.NewLine + Environment.NewLine + yaml);
  36. }
  37. private static void Collect(EvaluationResults node, List<string> errors) {
  38. if (node.Errors != null)
  39. foreach (KeyValuePair<string, string> error in node.Errors)
  40. errors.Add($"{node.InstanceLocation}: {error.Value}");
  41. if (node.Details != null)
  42. foreach (EvaluationResults child in node.Details)
  43. Collect(child, errors);
  44. }
  45. private static JsonElement ToJson(string yaml) {
  46. var stream = new YamlStream();
  47. stream.Load(new StringReader(yaml));
  48. using var document = JsonDocument.Parse(Convert(stream.Documents[0].RootNode));
  49. return document.RootElement.Clone();
  50. }
  51. private static string Convert(YamlNode node) {
  52. switch (node) {
  53. case YamlScalarNode scalar:
  54. if (scalar.Style is YamlDotNet.Core.ScalarStyle.SingleQuoted
  55. or YamlDotNet.Core.ScalarStyle.DoubleQuoted)
  56. return JsonSerializer.Serialize(scalar.Value);
  57. if (int.TryParse(scalar.Value, out var i))
  58. return i.ToString();
  59. if (double.TryParse(scalar.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
  60. return d.ToString(CultureInfo.InvariantCulture);
  61. if (bool.TryParse(scalar.Value, out var b))
  62. return b.ToString().ToLowerInvariant();
  63. return JsonSerializer.Serialize(scalar.Value);
  64. case YamlSequenceNode sequence:
  65. return "[" + string.Join(",", sequence.Children.Select(Convert)) + "]";
  66. case YamlMappingNode mapping:
  67. return "{" + string.Join(",", mapping.Children.Select(kvp =>
  68. JsonSerializer.Serialize(((YamlScalarNode)kvp.Key).Value) + ":" + Convert(kvp.Value))) + "}";
  69. default:
  70. return "null";
  71. }
  72. }
  73. }