Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace SAM.Analytical.Benchmark.Compare.Tests
/// the exit code and console output (cli-<mode>.txt, output directory written as <out>). Both routes
/// after the extraction must reproduce them byte for byte: the wrapper CLI, and a direct library call
/// (BenchmarkSerializer.Read + Query.Compare + Modify.WriteReports), which is how SAM_UI consumes it.
/// The Markdown golden was refreshed for the presentation-only two-decimal report; comparison.csv and
/// comparison-summary.json remain byte-identical to the pre-extraction output.
/// </summary>
[TestClass]
public class ExtractionRegressionTests
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

164 changes: 98 additions & 66 deletions SAM.Analytical.Benchmark.Compare.Tests/Fixtures/comparison.md

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions SAM.Analytical.Benchmark.Compare.Tests/PresentationFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors

using SAM.Analytical.Benchmark;
using SAM.Analytical.Benchmark.Compare;
using System;
using System.Globalization;
using System.Linq;

namespace SAM.Analytical.Benchmark.Compare.Tests
{
[TestClass]
public sealed class PresentationFormatTests
{
private static readonly ToleranceProfile Profile = ToleranceProfile.Default;

[TestMethod]
public void EngineeringQuantitiesUseTwoDecimalsAndHoursStayIntegers()
{
MetricComparison heating = Metric(
"consumptionHeating", MetricUnit.KilowattHour,
742.7750866578222, 2810.0182949034006,
2067.2432082455784, 0.7356689499121726,
ComparisonBand.Fail, NotApplicableReason.None);

Assert.AreEqual("742.78", PresentationFormat.Number(heating.TasValue, heating.Unit));
Assert.AreEqual("2810.02", PresentationFormat.Number(heating.OpenStudioValue, heating.Unit));
Assert.AreEqual("2067.24", PresentationFormat.Number(heating.AbsoluteDifference, heating.Unit));
Assert.AreEqual("73.57", PresentationFormat.Percent(heating.RelativeDifference));

MetricComparison peakHour = Metric(
"peakHeatingHour", MetricUnit.HourOfYear,
871, 919, 48, null,
ComparisonBand.Fail, NotApplicableReason.None, circular: true);

Assert.AreEqual("871", PresentationFormat.Number(peakHour.TasValue, peakHour.Unit));
Assert.AreEqual("919", PresentationFormat.Number(peakHour.OpenStudioValue, peakHour.Unit));
Assert.AreEqual("48", PresentationFormat.Number(peakHour.AbsoluteDifference, peakHour.Unit));
Assert.AreEqual(PresentationFormat.Unavailable, PresentationFormat.Percent(peakHour.RelativeDifference));
}

[TestMethod]
public void MissingAndNonFiniteValuesAreNeverZero()
{
Assert.AreEqual(PresentationFormat.Unavailable, PresentationFormat.Number(null, MetricUnit.KilowattHour));
Assert.AreEqual(PresentationFormat.Unavailable, PresentationFormat.Number(double.NaN, MetricUnit.KilowattHour));
Assert.AreEqual(PresentationFormat.Unavailable, PresentationFormat.Number(double.PositiveInfinity, MetricUnit.KilowattHour));
Assert.AreEqual(PresentationFormat.Unavailable, PresentationFormat.Percent(null));
Assert.AreEqual(string.Empty, PresentationFormat.Number(null, MetricUnit.KilowattHour, CultureInfo.InvariantCulture, string.Empty));
}

[TestMethod]
public void InformationalAndUnitMismatchNotesAreDistinct()
{
MetricComparison informational = Metric(
"peakHeatingHour", MetricUnit.HourOfYear,
871, 919, 48, null,
ComparisonBand.Fail, NotApplicableReason.None, circular: true);

string informationalNote = PresentationFormat.Note(informational);
StringAssert.Contains(informationalNote, PresentationFormat.InformationalNote);
Assert.IsFalse(informationalNote.Contains("Unit mismatch", StringComparison.Ordinal));

MetricComparison unitMismatch = Metric(
"consumptionHeating", MetricUnit.Kilowatt,
1, 2, null, null,
ComparisonBand.NotApplicable, NotApplicableReason.UnitMismatch,
tasUnit: MetricUnit.Kilowatt, openStudioUnit: MetricUnit.Watt);

string unitMismatchNote = PresentationFormat.Note(unitMismatch);
StringAssert.Contains(unitMismatchNote, "Unit mismatch (TAS kW vs OpenStudio W)");
Assert.IsFalse(unitMismatchNote.Contains(PresentationFormat.InformationalNote, StringComparison.Ordinal));
}

[TestMethod]
public void SpaceNameAndGuidAreDisplayedWithoutChangingAlignment()
{
SpaceComparison same = new SpaceComparison("cb68bed195af496bb255a13ffa5c58e9", "Office", "Office", SpaceMatchKind.Guid, Array.Empty<MetricComparison>());
Assert.AreEqual("Office", PresentationFormat.SpaceName(same));
Assert.AreEqual("cb68bed195af496bb255a13ffa5c58e9", PresentationFormat.SpaceGuid(same));

SpaceComparison different = new SpaceComparison("cb68bed195af496bb255a13ffa5c58e9", "Office", "Office 2", SpaceMatchKind.Guid, Array.Empty<MetricComparison>());
Assert.AreEqual("Office / Office 2", PresentationFormat.SpaceName(different));

SpaceComparison unnamed = new SpaceComparison(null, null, null, SpaceMatchKind.TasOnly, Array.Empty<MetricComparison>());
Assert.AreEqual("(unnamed)", PresentationFormat.SpaceName(unnamed));
Assert.AreEqual("(no GUID)", PresentationFormat.SpaceGuid(unnamed));
}

[TestMethod]
public void FailCountsSplitInformationalRowsFromContributingRows()
{
ComparisonResult result = Query.Compare(ScenarioDocuments.Tas(), ScenarioDocuments.OpenStudio(), Profile);

Assert.AreEqual(result.FailCount, result.ContributingFailCount + result.InformationalFailCount);
Assert.IsTrue(result.InformationalFailCount > 0);
Assert.IsTrue(result.ContributingFailCount > 0);
}

[TestMethod]
public void MarkdownExplainsPresentationAndKeepsRawReportsAtFullPrecision()
{
ComparisonResult result = Query.Compare(ScenarioDocuments.Tas(), ScenarioDocuments.OpenStudio(), Profile);
string markdown = Modify.WriteMarkdown(result);
string csv = Modify.WriteCsv(result);
string summary = Modify.WriteSummaryJson(result);

StringAssert.Contains(markdown, "Normalized difference (%) = 100 × |OpenStudio − TAS| / max(|TAS|, |OpenStudio|, applicable near-zero floor).");
StringAssert.Contains(markdown, "**not** percentage change against TAS");
StringAssert.Contains(markdown, "Match < 5.00%");
StringAssert.Contains(markdown, "Fail >= 15.00%");
StringAssert.Contains(markdown, PresentationFormat.InformationalNote);
StringAssert.Contains(markdown, "| Space | GUID |");
StringAssert.Contains(markdown, "`Design-day source: None` means no design-day source was supplied");
StringAssert.Contains(markdown, "Input limitation:");

// The CSV and JSON are the raw contract: the fraction and full precision must remain there.
StringAssert.Contains(csv, "0.6666666666666666");
StringAssert.Contains(csv, "66.66666666666666");
StringAssert.Contains(summary, "\"failRelative\": 0.15");
Assert.IsFalse(csv.Contains("66.67", StringComparison.Ordinal));
}

private static MetricComparison Metric(
string key,
MetricUnit unit,
double? tas,
double? openStudio,
double? absoluteDifference,
double? relativeDifference,
ComparisonBand band,
NotApplicableReason reason,
bool circular = false,
MetricUnit? tasUnit = null,
MetricUnit? openStudioUnit = null)
{
bool both = band != ComparisonBand.NotApplicable;
return new MetricComparison(
"model",
key,
unit,
tas,
both,
openStudio,
both,
circular,
absoluteDifference,
relativeDifference,
null,
band,
reason,
tasUnit ?? unit,
openStudioUnit ?? unit);
}
}
}
42 changes: 41 additions & 1 deletion SAM.Analytical.Benchmark.Compare.Tests/ReportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void ReportsMarkInformationalMetricsThatCarryABand()
Assert.IsTrue(Query.IsInformationalForNumericalGate(peakLoad));

string markdown = Modify.WriteMarkdown(result);
StringAssert.Contains(markdown, "| peakHeatingLoad | kW | 10 | 30 | 20 | 66.66666666666666 | Fail | informational (excluded from the numerical status) |");
StringAssert.Contains(markdown, "| peakHeatingLoad | kW | 10.00 | 30.00 | 20.00 | 66.67 | Fail | Informational — excluded from numerical status |");
StringAssert.Contains(markdown, "**excluded from the numerical status**");

StringAssert.Contains(Modify.WriteCsv(result), "model,peakHeatingLoad,kW,Both,10,30,20,");
Expand All @@ -127,6 +127,46 @@ public void ReportsMarkInformationalMetricsThatCarryABand()
StringAssert.Contains(Modify.WriteSummaryJson(result), "\"informational\": false");
}

[TestMethod]
public void MarkdownKeepsFractionalHourThresholdsAndAgreesWithTheRawProfile()
{
//Configurable hour thresholds are not hour-of-year indices. A profile may use 1.5 h / 24.5 h, and the Markdown must
//carry those values exactly as comparison.csv and comparison-summary.json do - never 1 / 24.
ToleranceProfile profile = new ToleranceProfile(
"fractional-hours",
warnRelative: 0.05,
failRelative: 0.15,
hourWarnAbsolute: 1.5,
hourFailAbsolute: 24.5,
nearZeroFloors: new System.Collections.Generic.Dictionary<MetricUnit, double>(),
defaultNearZeroFloor: 0);

ComparisonResult result = Query.Compare(ScenarioDocuments.Tas(), ScenarioDocuments.OpenStudio(), profile);

string markdown = Modify.WriteMarkdown(result);
string csv = Modify.WriteCsv(result);
string summary = Modify.WriteSummaryJson(result);

StringAssert.Contains(markdown, "| Peak-hour warn (h) | 1.5 |");
StringAssert.Contains(markdown, "| Peak-hour fail (h) | 24.5 |");
Assert.IsFalse(markdown.Contains("| Peak-hour warn (h) | 1 |", StringComparison.Ordinal));
Assert.IsFalse(markdown.Contains("| Peak-hour fail (h) | 24 |", StringComparison.Ordinal));

//The raw reports are the contract this presentation must agree with.
using (System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(summary))
{
System.Text.Json.JsonElement detail = document.RootElement.GetProperty("toleranceProfileDetail");
Assert.AreEqual(profile.HourWarnAbsolute, detail.GetProperty("hourWarnAbsolute").GetDouble());
Assert.AreEqual(profile.HourFailAbsolute, detail.GetProperty("hourFailAbsolute").GetDouble());
}

string[] lines = csv.TrimEnd('\n').Split('\n');
foreach (string line in lines.Skip(1))
{
StringAssert.Contains(line, ",1.5,24.5,0,");
}
}

[TestMethod]
public void GoldenReportsMatchCommittedFixturesByteForByte()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public ComparisonResult(

public int FailCount => AllMetrics.Count(metric => metric.Band == ComparisonBand.Fail);

/// <summary>Fail bands that are informational; they are shown but excluded from the numerical status.</summary>
public int InformationalFailCount => AllMetrics.Count(metric => metric.Band == ComparisonBand.Fail && Query.IsInformationalForNumericalGate(metric));

/// <summary>Fail bands that contribute to the numerical status. Always <see cref="FailCount"/> minus <see cref="InformationalFailCount"/>.</summary>
public int ContributingFailCount => FailCount - InformationalFailCount;

public int NotApplicableCount => AllMetrics.Count(metric => metric.Band == ComparisonBand.NotApplicable);

/// <summary>
Expand Down
Loading
Loading