<Fix>: Correct test-dependent result evaluation in ResultsWriter, increase revision to 3.9.3145.101

Cause:

- ResultsWriter evaluated selected test-dependent items without the actual TestID.
- This caused values such as Test passed(), timestamps, flow, pressure, temperature, conductivity and error data to be empty or incorrect.

Solution:

1. Fixed test-aware result evaluation
   - Uses published regular meter test results.
   - Passes mtr.Name() as TestID to item.Print(wm, testId).

2. Restored correct test result mapping
   - Test-dependent values are now resolved from the correct TestRslt / MeterTestRslt.

3. Increased revision
   - Updated revision to 3.9.3145.101.
This commit is contained in:
Marek Frniak 2026-09-02 13:25:45 +02:00
parent 054075a341
commit d3c8813012
2 changed files with 512 additions and 166 deletions

View File

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// Build Number
// Revision
//
[assembly: AssemblyVersion("3.9.3145.100")]
[assembly: AssemblyFileVersion("3.9.3145.100")]
[assembly: AssemblyVersion("3.9.3145.101")]
[assembly: AssemblyFileVersion("3.9.3145.101")]

View File

@ -242,6 +242,13 @@ namespace TBF.Rig.Output.DB.ResultsWriter
/// <summary>
/// Writes all enabled water meters in a batch.
/// </summary>
/// <remarks>
/// XML payload targets are generated once per batch. Every enabled water
/// meter contributes one logical repeat record to the same XML document.
///
/// Legacy Insert output keeps its existing behavior: every published
/// regular meter test is written with its concrete test ID context.
/// </remarks>
public void WriteBatchResults(
Batch batch)
{
@ -256,11 +263,25 @@ namespace TBF.Rig.Output.DB.ResultsWriter
WriterCfg storageCfg =
GetDataStorageWriterConfiguration();
XmlPayloadRequestBuilder payloadBuilder =
storageCfg.UsesXmlPayload()
? new XmlPayloadRequestBuilder(
storageCfg)
: null;
if (storageCfg.UsesXmlPayload())
{
WriteXmlBatchResults(
batch,
storageCfg);
return;
}
if (storageCfg.WriteMode !=
WriteMode.Insert)
{
throw new NotSupportedException(
string.Format(
"ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode));
}
foreach (WaterMeter wm
in batch.WaterMeters)
@ -271,104 +292,141 @@ namespace TBF.Rig.Output.DB.ResultsWriter
continue;
}
DataWriteRequest targetRequest;
if (storageCfg.IsStoredProcedurePayloadTarget())
{
PayloadGenerationRequest generationRequest =
BuildPayloadGenerationRequest(
wm);
XmlPayloadBuildResult buildResult =
payloadBuilder.Build(
generationRequest,
Convert.ToString(
wm.SerialNr));
targetRequest =
buildResult.Request;
if (!string.IsNullOrWhiteSpace(
buildResult.ArchiveFilePath))
{
log.InfoFormat(
"XML payload archived to '{0}'.",
buildResult.ArchiveFilePath);
}
}
else if (storageCfg.IsXmlFileTarget())
{
PayloadGenerationRequest generationRequest =
BuildPayloadGenerationRequest(
wm);
XmlPayloadBuildResult buildResult =
payloadBuilder.BuildFile(
generationRequest,
Convert.ToString(
wm.SerialNr));
targetRequest =
buildResult.Request;
log.InfoFormat(
"XML file payload generated for WM position {0}: {1}",
wm.WMPosition,
buildResult.OutputFileName);
}
else if (storageCfg.WriteMode ==
WriteMode.Insert)
{
targetRequest =
BuildInsertRequest(
wm);
if (targetRequest.InsertItems.Count == 0)
{
log.WarnFormat(
"No values to write for WM position {0}",
wm.WMPosition);
continue;
}
}
else
{
throw new NotSupportedException(
string.Format(
"ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode));
}
WriterDiagnosticResult result =
dataStorageWriter.SetData(
targetRequest);
if (!result.Success)
{
throw new Exception(
result.Message);
}
log.WarnFormat(
"ResultsWriter wrote WM position {0}. Target={1}, Technology={2}, Mode={3}.",
wm.WMPosition,
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode);
//
// Insert mode needs the same test context as the legacy TBF
// result writers. One row is generated for every published
// regular meter test and all selected items are evaluated
// with the actual mtr.Name() as test ID.
//
WriteInsertResults(
wm,
storageCfg);
}
}
/// <summary>
/// Generates and writes one XML payload for the complete batch.
/// </summary>
private void WriteXmlBatchResults(
Batch batch,
WriterCfg storageCfg)
{
IList<WaterMeter> enabledMeters =
batch.WaterMeters
.Where(
wm =>
wm != null &&
!wm.Disabled)
.ToList();
if (enabledMeters.Count == 0)
return;
XmlPayloadRequestBuilder payloadBuilder =
new XmlPayloadRequestBuilder(
storageCfg);
PayloadGenerationRequest generationRequest =
BuildBatchPayloadGenerationRequest(
enabledMeters);
string payloadIdentifier =
BuildBatchPayloadIdentifier(
enabledMeters);
XmlPayloadBuildResult buildResult;
if (storageCfg.IsStoredProcedurePayloadTarget())
{
buildResult =
payloadBuilder.Build(
generationRequest,
payloadIdentifier);
}
else if (storageCfg.IsXmlFileTarget())
{
buildResult =
payloadBuilder.BuildFile(
generationRequest,
payloadIdentifier);
}
else
{
throw new NotSupportedException(
string.Format(
"Unsupported XML payload target '{0}' / '{1}' / '{2}'.",
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode));
}
if (!string.IsNullOrWhiteSpace(
buildResult.ArchiveFilePath))
{
log.InfoFormat(
"XML payload archived to '{0}'.",
buildResult.ArchiveFilePath);
}
if (!string.IsNullOrWhiteSpace(
buildResult.OutputFileName))
{
log.InfoFormat(
"XML batch payload generated: {0}",
buildResult.OutputFileName);
}
WriterDiagnosticResult result =
dataStorageWriter.SetData(
buildResult.Request);
if (!result.Success)
{
throw new Exception(
result.Message);
}
log.WarnFormat(
"ResultsWriter wrote one XML payload for {0} enabled water meter(s). Target={1}, Technology={2}, Mode={3}.",
enabledMeters.Count,
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode);
}
/// <summary>
/// Creates a stable identifier used by the XML request builder for one batch.
/// </summary>
private string BuildBatchPayloadIdentifier(
IList<WaterMeter> enabledMeters)
{
if (enabledMeters == null ||
enabledMeters.Count == 0)
{
return "BATCH";
}
string firstSerial =
Convert.ToString(
enabledMeters[0].SerialNr);
if (string.IsNullOrWhiteSpace(
firstSerial))
{
return "BATCH";
}
return firstSerial;
}
/// <summary>
/// Generates a dry-run XML payload without calling the database.
/// </summary>
/// <param name="simulationBatch">
/// Batch used to evaluate one-time TBF mappings.
/// Batch used to evaluate one-time TBF mappings and provide preview context.
/// </param>
/// <param name="simulatedTestCount">
/// Number of repeated GROUP/TEST instances generated for the preview.
/// Number of logical repeated result records generated for the preview.
/// </param>
public XmlPayloadBuildResult GeneratePreviewPayload(
Batch simulationBatch,
@ -427,10 +485,70 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
/// <summary>
/// Creates the legacy insert request.
/// Writes Insert rows for one water meter using the actual TBF test context.
/// </summary>
/// <remarks>
/// One row is written for every published regular meter test.
/// The actual test ID is passed to WMeterRsltItemSpec.Print().
/// </remarks>
private void WriteInsertResults(
WaterMeter wm,
WriterCfg storageCfg)
{
IList<MeterTestRslt> publishedTests =
wm.RegularMeterTestRslts()
.Where(
mtr =>
mtr != null &&
mtr.Publish() ==
Publish.Always)
.ToList();
//
// Preserve meter/batch data even if there is no published test.
//
if (publishedTests.Count == 0)
{
DataWriteRequest fallbackRequest =
BuildInsertRequest(
wm,
string.Empty);
WriteInsertRequest(
wm,
string.Empty,
fallbackRequest,
storageCfg);
return;
}
foreach (MeterTestRslt mtr
in publishedTests)
{
string testId =
mtr.Name() ??
string.Empty;
DataWriteRequest request =
BuildInsertRequest(
wm,
testId);
WriteInsertRequest(
wm,
testId,
request,
storageCfg);
}
}
/// <summary>
/// Builds one Insert request for one water meter and one concrete test.
/// </summary>
private DataWriteRequest BuildInsertRequest(
WaterMeter wm)
WaterMeter wm,
string testId)
{
DataWriteRequest request =
new DataWriteRequest
@ -441,7 +559,7 @@ namespace TBF.Rig.Output.DB.ResultsWriter
if (resultsWriterCfg.SelectedItems == null)
return request;
foreach (var item
foreach (Results.WMeterRsltItemSpec item
in resultsWriterCfg.SelectedItems)
{
if (item == null ||
@ -458,7 +576,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption,
Value =
item.Print(wm)
item.Print(
wm,
testId)
});
}
@ -466,74 +586,290 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
/// <summary>
/// Creates the runtime XML mapping request for one meter.
/// Executes one prepared Insert request.
/// </summary>
private PayloadGenerationRequest BuildPayloadGenerationRequest(
WaterMeter wm)
private void WriteInsertRequest(
WaterMeter wm,
string testId,
DataWriteRequest request,
WriterCfg storageCfg)
{
if (request == null ||
request.InsertItems.Count == 0)
{
log.WarnFormat(
"No values to write for WM position {0}",
wm.WMPosition);
return;
}
WriterDiagnosticResult result =
dataStorageWriter.SetData(
request);
if (!result.Success)
{
throw new Exception(
result.Message);
}
log.WarnFormat(
"ResultsWriter wrote WM position {0}, TestID='{1}'. Target={2}, Technology={3}, Mode={4}.",
wm.WMPosition,
testId,
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode);
}
/// <summary>
/// Creates one runtime XML mapping request for the complete batch.
/// </summary>
/// <param name="enabledMeters">
/// Enabled water meters that must be represented in the generated payload.
/// </param>
/// <remarks>
/// <para>
/// Every enabled water meter creates one logical repeat record. All
/// destinations located inside the repeating XML prototype are populated
/// into that same record.
/// </para>
///
/// <para>
/// Result evaluation first uses the meter-level context. If that does not
/// produce a value, the same result item is evaluated against the actual
/// published measurement-test IDs in procedure order. This preserves
/// meter-level values while also allowing test-result variables to be
/// populated in the XML record.
/// </para>
/// </remarks>
private PayloadGenerationRequest BuildBatchPayloadGenerationRequest(
IList<WaterMeter> enabledMeters)
{
PayloadGenerationRequest request =
new PayloadGenerationRequest();
if (resultsWriterCfg.SelectedItems == null)
return request;
int sourceIndex = 0;
foreach (var item
in resultsWriterCfg.SelectedItems)
if (enabledMeters == null ||
enabledMeters.Count == 0 ||
resultsWriterCfg.SelectedItems == null)
{
if (item == null ||
string.IsNullOrWhiteSpace(
item.Caption))
{
continue;
}
return request;
}
List<Results.WMeterRsltItemSpec> singleItems =
resultsWriterCfg.SelectedItems
.Where(
item =>
item != null &&
!string.IsNullOrWhiteSpace(
item.Caption) &&
!IsRepeatDestination(
item.Caption))
.ToList();
List<Results.WMeterRsltItemSpec> repeatItems =
resultsWriterCfg.SelectedItems
.Where(
item =>
item != null &&
!string.IsNullOrWhiteSpace(
item.Caption) &&
IsRepeatDestination(
item.Caption))
.ToList();
//
// One-time XML destinations exist only once in the document.
// Evaluate them from the first enabled water meter. This preserves
// the existing one-time mapping model while repeat destinations are
// created once for every enabled water meter.
//
WaterMeter firstMeter =
enabledMeters[0];
for (int singleIndex = 0;
singleIndex < singleItems.Count;
singleIndex++)
{
Results.WMeterRsltItemSpec item =
singleItems[
singleIndex];
string sourceKey =
"Result." +
sourceIndex.ToString(
CultureInfo.InvariantCulture);
string.Format(
CultureInfo.InvariantCulture,
"Batch.Single.{0}",
singleIndex);
sourceIndex++;
request.SingleMappings.Add(
new PayloadMapping
{
SourceKey =
sourceKey,
if (IsRepeatDestination(
item.Caption))
DestinationPath =
item.Caption
});
request.SingleValues.SetValue(
sourceKey,
EvaluateXmlMappedValue(
firstMeter,
item));
}
//
// Every enabled water meter becomes one logical repeat record.
//
for (int meterIndex = 0;
meterIndex < enabledMeters.Count;
meterIndex++)
{
WaterMeter wm =
enabledMeters[
meterIndex];
PayloadRepeatRecord record =
new PayloadRepeatRecord();
for (int repeatIndex = 0;
repeatIndex < repeatItems.Count;
repeatIndex++)
{
PayloadRepeatRecord record =
new PayloadRepeatRecord();
Results.WMeterRsltItemSpec item =
repeatItems[
repeatIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
"Batch.Repeat.{0}.{1}",
meterIndex,
repeatIndex);
AddRecordMapping(
record,
sourceKey,
item.Caption,
item.Print(wm));
EvaluateXmlMappedValue(
wm,
item));
}
if (record.Mappings.Count > 0)
{
request.RepeatRecords.Add(
record);
}
else
{
request.SingleMappings.Add(
new PayloadMapping
{
SourceKey =
sourceKey,
DestinationPath =
item.Caption
});
request.SingleValues.SetValue(
sourceKey,
item.Print(wm));
}
}
return request;
}
/// <summary>
/// Creates a preview request with an exact number of simulated tests.
/// Evaluates one configured result item for XML output.
/// </summary>
/// <remarks>
/// Meter/batch variables normally return a value directly from
/// Print(wm). Test-result variables require a concrete test ID. For those
/// variables the method evaluates the published regular meter tests in
/// measurement-procedure order and uses the first non-empty value.
///
/// This fallback is intentionally applied only when the meter-level
/// evaluation is empty.
/// </remarks>
private string EvaluateXmlMappedValue(
WaterMeter wm,
Results.WMeterRsltItemSpec item)
{
if (wm == null ||
item == null)
{
return string.Empty;
}
string meterValue =
item.Print(
wm);
if (!string.IsNullOrWhiteSpace(
meterValue))
{
return meterValue;
}
IList<MeterTestRslt> publishedTests =
wm.RegularMeterTestRslts()
.Where(
mtr =>
mtr != null &&
mtr.Publish() ==
Publish.Always)
.ToList();
string firstResolvedValue =
string.Empty;
string firstResolvedTestId =
string.Empty;
foreach (MeterTestRslt mtr
in publishedTests)
{
string testId =
mtr.Name() ??
string.Empty;
string testValue =
item.Print(
wm,
testId);
if (string.IsNullOrWhiteSpace(
testValue))
{
continue;
}
if (string.IsNullOrWhiteSpace(
firstResolvedValue))
{
firstResolvedValue =
testValue;
firstResolvedTestId =
testId;
continue;
}
//
// A single XML destination can hold only one scalar value.
// If more than one test produces a different value for the same
// mapped result, preserve deterministic procedure order and keep
// the first value, but make the ambiguity visible in the log.
//
if (!string.Equals(
firstResolvedValue,
testValue,
StringComparison.Ordinal))
{
log.WarnFormat(
"XML mapping '{0}' produced values in more than one test context for WM position {1}. Using first test '{2}'.",
item.Caption,
wm.WMPosition,
firstResolvedTestId);
break;
}
}
return firstResolvedValue;
}
/// <summary>
/// Creates a preview request with an exact number of simulated repeat records.
/// </summary>
private PayloadGenerationRequest BuildPreviewGenerationRequest(
WaterMeter wm,
@ -563,10 +899,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption))
{
//
// Repeated XML attributes have no predefined semantics.
// Keep the selected TBF result exactly as configured:
// its evaluated value is written only to its selected
// repeating destination.
// A repeating mapping defines a destination inside the
// repeat prototype. The mapping itself must not create a
// separate repeat record.
//
repeatItems.Add(
item);
@ -584,11 +919,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
wm);
//
// A minimal simulation batch does not contain every
// possible TBF result source. Never leave a configured
// preview destination empty only because its runtime
// simulation source is unavailable. Use a valid,
// type-appropriate and clearly synthetic value instead.
// A minimal simulation batch does not contain every possible
// TBF result source. Use a clearly synthetic type-appropriate
// value when the evaluated preview value is unavailable.
//
if (string.IsNullOrWhiteSpace(
previewValue))
@ -617,25 +950,38 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
}
if (repeatItems.Count > 0)
//
// Create the requested number of logical repeat records. Every record
// receives all configured repeating mappings, so one simulated record
// is structurally equivalent to one generated runtime record.
//
for (int recordIndex = 0;
recordIndex < simulatedTestCount &&
repeatItems.Count > 0;
recordIndex++)
{
for (int testIndex = 0;
testIndex < simulatedTestCount;
testIndex++)
PayloadRepeatRecord record =
new PayloadRepeatRecord();
for (int repeatIndex = 0;
repeatIndex < repeatItems.Count;
repeatIndex++)
{
Results.WMeterRsltItemSpec item =
repeatItems[
testIndex %
repeatItems.Count];
PayloadRepeatRecord record =
new PayloadRepeatRecord();
repeatIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
"Preview.Repeat.{0}",
testIndex);
"Preview.Repeat.{0}.{1}",
recordIndex,
repeatIndex);
int simulationValueIndex =
recordIndex *
repeatItems.Count +
repeatIndex;
AddRecordMapping(
record,
@ -643,11 +989,11 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption,
CreateUniversalSimulationValue(
item,
testIndex));
request.RepeatRecords.Add(
record);
simulationValueIndex));
}
request.RepeatRecords.Add(
record);
}
return request;