From 72aac8c4cfe9aa32498bf536b05d908ac31861bf Mon Sep 17 00:00:00 2001
From: Archie Miller <62433534+Archie-Miller@users.noreply.github.com>
Date: Tue, 7 Jan 2025 12:39:56 -0700
Subject: [PATCH] Address Issue 85 - Convertor should support arrays for
DisplayName and Description (#87)
* Setup Test file for testing
* Write test files
Fix test files that were broken by accident
* Testing complete for Localized Texts
---
NodeSetToAML.cs | 114 ++++++++-
Opc2AmlConsole/Properties/launchSettings.json | 3 +-
SystemTest/NodeSetFiles/TestAml.xml | 147 ++++++++++-
SystemTest/TestLocalizedText.cs | 231 ++++++++++++++++++
4 files changed, 476 insertions(+), 19 deletions(-)
create mode 100644 SystemTest/TestLocalizedText.cs
diff --git a/NodeSetToAML.cs b/NodeSetToAML.cs
index ec93bb2..0939b44 100644
--- a/NodeSetToAML.cs
+++ b/NodeSetToAML.cs
@@ -586,27 +586,109 @@ private void AddBaseNodeClassAttributes( AttributeSequence seq, UANode uanode, U
uriatt.Value = myuri;
}
- if (uanode.DisplayName != null &&
- uanode.DisplayName.Length > 0 &&
- uanode.DisplayName[0].Value != uanode.DecodedBrowseName.Name)
+ BuildLocalizedTextAttribute( seq, "DisplayName", uanode.DisplayName,
+ uanode.DecodedBrowseName.Name, ignoreEqual: true );
+ BuildLocalizedTextAttribute( seq, "Description", uanode.Description,
+ uanode.DecodedBrowseName.Name, ignoreEqual: false );
+
+ UAType uaType = uanode as UAType;
+ if ( uaType != null && uaType.IsAbstract )
{
- AddModifyAttribute(seq, "DisplayName", "LocalizedText",
- uanode.DisplayName[0].Value);
+ AddModifyAttribute(seq, "IsAbstract", "Boolean", uaType.IsAbstract);
}
+ }
- if (uanode.Description != null &&
- uanode.Description.Length > 0 &&
- uanode.Description[0].Value.Length > 0)
+ private void BuildLocalizedTextAttribute(
+ AttributeSequence seq,
+ string attributeName,
+ NodeSet.LocalizedText[] localizedTexts,
+ string equalityString, bool ignoreEqual )
+ {
+ if( localizedTexts != null )
{
- AddModifyAttribute(seq, "Description", "LocalizedText",
- uanode.Description[0].Value);
+ if( localizedTexts.Length > 1 )
+ {
+ AddModifyAttribute( seq, attributeName, "LocalizedText", localizedTexts[ 0 ].Value );
+
+ AttributeType displayNameAttribute = seq[ attributeName ];
+ if( displayNameAttribute != null )
+ {
+ string previousLocaleId = string.Empty;
+ string defaultLocaleId = GetLocaleId( localizedTexts[ 0 ], ref previousLocaleId );
+ AttributeType arrayRoot = AddModifyAttribute( displayNameAttribute.Attribute, defaultLocaleId, "String",
+ localizedTexts[ 0 ].Value );
+
+ // Redo first element
+ previousLocaleId = string.Empty;
+ for( int index = 0; index < localizedTexts.Length; index++ )
+ {
+ string localeId = GetLocaleId( localizedTexts[ index ], ref previousLocaleId );
+ AddModifyAttribute( arrayRoot.Attribute, "aml-lang=" + localeId,
+ "String", localizedTexts[ index ].Value );
+ }
+ }
+ }
+ else if( localizedTexts.Length > 0 )
+ {
+ NodeSet.LocalizedText localizedText = localizedTexts[ 0 ];
+ if( ignoreEqual == false ||
+ localizedText.Value != equalityString )
+ {
+ AttributeType root = AddModifyAttribute( seq, attributeName, "LocalizedText", localizedText.Value );
+
+ if( !String.IsNullOrEmpty( localizedText.Locale ) )
+ {
+ AddModifyAttribute( root.Attribute, localizedText.Locale, "LocalizedText", localizedText.Value );
+ }
+ }
+ }
}
+ }
- UAType uaType = uanode as UAType;
- if ( uaType != null && uaType.IsAbstract )
+ private string GetLocaleId( NodeSet.LocalizedText localizedText, ref string lastUnknownLocale )
+ {
+ string localeId = string.Empty;
+
+ if ( localizedText != null )
{
- AddModifyAttribute(seq, "IsAbstract", "Boolean", uaType.IsAbstract);
+ if ( String.IsNullOrEmpty( localizedText.Locale ) )
+ {
+ if ( String.IsNullOrEmpty( lastUnknownLocale ) )
+ {
+ localeId = "qaa";
+ }
+ else
+ {
+ if ( lastUnknownLocale.Length == 3 && lastUnknownLocale[0] == 'q' )
+ {
+ char secondChar = lastUnknownLocale[1];
+ char lastChar = lastUnknownLocale[ 2 ];
+ if ( lastChar == 'z' )
+ {
+ // It's pretty impractical to have 20*26 unknown locales for a single node.
+ if( secondChar < 't' )
+ {
+ secondChar++;
+ localeId = "q" + secondChar + 'a';
+ }
+ }
+ else
+ {
+ localeId = "q" + secondChar + (char)( lastChar + 1 );
+ }
+ }
+ }
+ lastUnknownLocale = localeId;
+ }
+ else
+ {
+ localeId = localizedText.Locale;
+ }
+
+ return localeId;
}
+
+ return localeId;
}
private AttributeType AddModifyAttribute(AttributeSequence seq, string name, string refDataType, Variant val, bool bListOf = false, string sURI = uaNamespaceURI)
@@ -910,7 +992,13 @@ private AttributeType AddModifyAttribute(AttributeSequence seq, string name, str
if( localizedText != null && localizedText.Text != null )
{
a.DefaultAttributeValue = a.AttributeValue = localizedText.Text;
+ if ( !string.IsNullOrEmpty( localizedText.Locale ) )
+ {
+ AddModifyAttribute(a.Attribute, localizedText.Locale,
+ "String", localizedText.Text);
+ }
}
+
break;
}
diff --git a/Opc2AmlConsole/Properties/launchSettings.json b/Opc2AmlConsole/Properties/launchSettings.json
index db1f0ae..71b4db4 100644
--- a/Opc2AmlConsole/Properties/launchSettings.json
+++ b/Opc2AmlConsole/Properties/launchSettings.json
@@ -1,7 +1,8 @@
{
"profiles": {
"Opc2AmlConsole": {
- "commandName": "Project"
+ "commandName": "Project",
+ "commandLineArgs": "--Nodeset TestAml.xml"
}
}
}
\ No newline at end of file
diff --git a/SystemTest/NodeSetFiles/TestAml.xml b/SystemTest/NodeSetFiles/TestAml.xml
index 13c3d7d..06e97ef 100644
--- a/SystemTest/NodeSetFiles/TestAml.xml
+++ b/SystemTest/NodeSetFiles/TestAml.xml
@@ -2126,7 +2126,8 @@
- OneDimension
+ OneDimensionArray
+ A One Dimensional Array
ns=1;i=5011
i=63
@@ -2147,7 +2148,10 @@
- TwoDimensions
+ TwoDimensionArray
+ aTwoDimensionalArray
+ A Different Two Dimension Array
+ Another Two Dimension Array
ns=1;i=5011
i=63
@@ -2168,7 +2172,10 @@
- FiveDimensions
+ FiveDimensionArray
+ A Five DimensionArray
+ Another Five Dimension Array
+
ns=1;i=5011
i=63
@@ -3087,13 +3094,143 @@
+
+ Localized Text Attributes
+
+ i=85
+ i=61
+
+
+
+
+
+ i=63
+ ns=1;i=5024
+
+
+
+
+ Single Localized Text
+ A Single Description with no Locale
+
+ i=63
+ ns=1;i=5024
+
+
+
+ Attributes have display name and description, but no Locale
+
+
+
+
+ Single Localized Text with Locale
+ A Single Description with a Locale
+
+ i=63
+ ns=1;i=5024
+
+
+
+ en
+ Attributes have display name and description with a single Locale
+
+
+
+
+ First DisplayName with no Locale
+ First Description with no Locale
+ Second DisplayName with a Locale
+ Second Description with a Locale
+ Troisième texte localisé avec paramètres régionaux
+ Troisième description avec un paramètre régional
+ Letzter lokalisierter Text mit Gebietsschema
+ Letzte Beschreibung mit einem Gebietsschema
+
+ i=63
+ ns=1;i=5024
+
+
+
+
+ Multiple
+
+
+ en
+ DisplayNames and Descriptions
+
+
+ en
+ first without a locale
+
+
+
+
+
+
+ First DisplayName with a Locale
+ First Description with a Locale
+ Deuxième DisplayName avec une locale
+ Deuxième description avec une locale
+ Dritter DisplayName mit einem Gebietsschema
+ Dritte Beschreibung mit einem Gebietsschema
+ Last DisplayName with no Locale
+ Last Description with no Locale
+
+ i=63
+ ns=1;i=5024
+
+
+
+
+ en
+ Multiple
+
+
+ en
+ DisplayNames and Descriptions
+
+
+ last without a locale
+
+
+
+
+
+
+ First DisplayName with no Locale
+ First Description with no Locale
+ Second DisplayName with no Locale
+ Second Description with no Locale
+ Third DisplayName with no Locale
+ Third Description with no Locale
+ Last DisplayName with no Locale
+ Last Description with no Locale
+
+ i=63
+ ns=1;i=5024
+
+
+
+
+ Multiple
+
+
+ DisplayNames and Descriptions
+
+
+ all without a locale
+
+
+
+
+
diff --git a/SystemTest/TestLocalizedText.cs b/SystemTest/TestLocalizedText.cs
new file mode 100644
index 0000000..c5934a6
--- /dev/null
+++ b/SystemTest/TestLocalizedText.cs
@@ -0,0 +1,231 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.IO;
+using Aml.Engine.AmlObjects;
+using Aml.Engine.CAEX;
+using Aml.Engine.CAEX.Extensions;
+using Opc.Ua;
+using static System.Net.Mime.MediaTypeNames;
+
+
+namespace SystemTest
+{
+ [TestClass]
+ public class TestLocalizedText
+ {
+ CAEXDocument m_document = null;
+
+ #region Tests
+
+ [TestMethod]
+ public void TestNoValue( )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( "6227" );
+ AttributeType attributeType = objectToTest.Attribute[ "Value" ];
+ Assert.IsNotNull( attributeType );
+ Assert.IsNull( attributeType.Value, "Unexpected Value found" );
+ }
+
+ [TestMethod]
+ [DataRow( "6228", "", "Attributes have display name and description, but no Locale",
+ DisplayName = "Single value no Locale" )]
+ [DataRow( "6229", "en", "Attributes have display name and description with a single Locale",
+ DisplayName = "Single value with Locale" )]
+ public void TestScalarValue( string node, string locale, string text )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( node );
+ AttributeType value = ValidateAttribute( objectToTest.Attribute, "Value", text );
+
+ if( string.IsNullOrEmpty( locale ) )
+ {
+ Assert.AreEqual( 0, value.Attribute.Count );
+ }
+ else
+ {
+ ValidateAttribute( value.Attribute, locale, text );
+ }
+ }
+
+ [TestMethod]
+ [DataRow( "6230", new string[] { "", "en", "en" },
+ new string[] {
+ "Multiple",
+ "DisplayNames and Descriptions",
+ "first without a locale" },
+ DisplayName = "Multiple Starting with no Locale" )]
+
+ [DataRow( "6231", new string[] { "en", "en", "" },
+ new string[] {
+ "Multiple",
+ "DisplayNames and Descriptions",
+ "last without a locale" },
+ DisplayName = "Multiple Ending with no Locale" )]
+
+ [DataRow( "6232", new string[] { "", "", "" },
+ new string[] {
+ "Multiple",
+ "DisplayNames and Descriptions",
+ "all without a locale" },
+ DisplayName = "Multiple all without no Locale" )]
+
+ public void TestArrayValue( string node, string[] locales, string[] values )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( node );
+ AttributeType attributeType = objectToTest.Attribute[ "Value" ];
+ Assert.IsNotNull( attributeType );
+ Assert.IsNull( attributeType.Value, "Unexpected Value found" );
+
+ for( int index = 0; index < locales.Length; index++ )
+ {
+ AttributeType indexed = ValidateAttribute( attributeType.Attribute, index.ToString(), values[ index ] );
+ if( string.IsNullOrEmpty( locales[ index ] ) )
+ {
+ Assert.AreEqual( 0, indexed.Attribute.Count, "Unexpected Locale found" );
+ }
+ else
+ {
+ ValidateAttribute( indexed.Attribute, locales[ index ], values[ index ] );
+ }
+ }
+ }
+
+ [TestMethod]
+ [DataRow( "DisplayName", DisplayName = "No LocalizedText DisplayName" )]
+ [DataRow( "Description", DisplayName = "No LocalizedText Description" )]
+ public void NoLocalizedText( string attributeName )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( "6227" );
+ AttributeType attributeType = objectToTest.Attribute[ attributeName ];
+ Assert.IsNull( attributeType, "Unexpected attribute found for " + attributeName );
+ }
+
+
+ [TestMethod]
+ [DataRow("6228", "DisplayName", "", "Single Localized Text",
+ DisplayName = "Single no Locale DisplayName")]
+ [DataRow( "6228", "Description", "", "A Single Description with no Locale",
+ DisplayName = "Single no Locale Description" )]
+ [DataRow( "6229", "DisplayName", "en", "Single Localized Text with Locale",
+ DisplayName = "Single with Locale DisplayName" )]
+ [DataRow( "6229", "Description", "en", "A Single Description with a Locale",
+ DisplayName = "Single with Locale Description" )]
+ public void SingleLocalizedText(string nodeId, string attributeName, string localeId, string text )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( nodeId );
+ AttributeType attributeType = ValidateAttribute( objectToTest.Attribute, attributeName, text );
+
+ if( string.IsNullOrEmpty( localeId ) )
+ {
+ Assert.AreEqual( 0, attributeType.Attribute.Count, 1 );
+ }
+ else
+ {
+ ValidateAttribute( attributeType.Attribute, localeId, text );
+ }
+ }
+
+ [TestMethod]
+ [DataRow( "6230", "DisplayName",
+ new string[] { "qaa", "en", "fr", "de"},
+ new string[] {
+ "First DisplayName with no Locale",
+ "Second DisplayName with a Locale",
+ "Troisième texte localisé avec paramètres régionaux",
+ "Letzter lokalisierter Text mit Gebietsschema" },
+ DisplayName = "Multiple Starting with no Locale DisplayName" )]
+ [DataRow( "6230", "Description",
+ new string[] { "qaa", "en", "fr", "de" },
+ new string[] {
+ "First Description with no Locale",
+ "Second Description with a Locale",
+ "Troisième description avec un paramètre régional",
+ "Letzte Beschreibung mit einem Gebietsschema" },
+ DisplayName = "Multiple Starting with no Locale Description" )]
+ [DataRow( "6231", "DisplayName",
+ new string[] { "en", "fr", "de", "qaa"},
+ new string[] {
+ "First DisplayName with a Locale",
+ "Deuxième DisplayName avec une locale",
+ "Dritter DisplayName mit einem Gebietsschema",
+ "Last DisplayName with no Locale" },
+ DisplayName = "Multiple Ending with no Locale DisplayName" )]
+ [DataRow( "6231", "Description",
+ new string[] { "en", "fr", "de", "qaa" },
+ new string[] {
+ "First Description with a Locale",
+ "Deuxième description avec une locale",
+ "Dritte Beschreibung mit einem Gebietsschema",
+ "Last Description with no Locale" },
+ DisplayName = "Multiple Ending with no Locale Description" )]
+
+
+ [DataRow( "6232", "DisplayName",
+ new string[] { "qaa", "qab", "qac", "qad" },
+ new string[] {
+ "First DisplayName with no Locale",
+ "Second DisplayName with no Locale",
+ "Third DisplayName with no Locale",
+ "Last DisplayName with no Locale" },
+ DisplayName = "Multiple no Locale DisplayName" )]
+ [DataRow( "6232", "Description",
+ new string[] { "qaa", "qab", "qac", "qad" },
+ new string[] {
+ "First Description with no Locale",
+ "Second Description with no Locale",
+ "Third Description with no Locale",
+ "Last Description with no Locale" },
+ DisplayName = "Multiple no Locale Description" )]
+ public void MultipleLocalizedText( string nodeId, string attributeName, string[] localeId, string[] text )
+ {
+ SystemUnitClassType objectToTest = GetTestObject( nodeId );
+ AttributeType topLevel = ValidateAttribute( objectToTest.Attribute, attributeName, text[ 0 ] );
+ AttributeType textLevel = ValidateAttribute( topLevel.Attribute, localeId[0], text[ 0 ] );
+
+ for( int index = 0; index < localeId.Length; index++ )
+ {
+ string subLocale = "aml-lang=" + localeId[ index ];
+ ValidateAttribute( textLevel.Attribute, subLocale, text[index] );
+ }
+ }
+
+ private AttributeType ValidateAttribute( AttributeSequence sequence, string attributeName, string attributeValue )
+ {
+ Assert.IsNotNull( sequence, "AttributeSequence not found" );
+ AttributeType attributeType = sequence[ attributeName ];
+ Assert.IsNotNull( attributeType, attributeName + " attribute not found" );
+ Assert.IsNotNull( attributeType.Value, attributeName + " attribute valid not found" );
+ Assert.AreEqual( attributeValue, attributeType.Value, "Unexpected value for " + attributeName );
+ return attributeType;
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private CAEXDocument GetDocument()
+ {
+ if( m_document == null )
+ {
+ m_document = TestHelper.GetReadOnlyDocument( "TestAml.xml.amlx" );
+ }
+ Assert.IsNotNull( m_document, "Unable to retrieve Document" );
+ return m_document;
+ }
+
+ public SystemUnitClassType GetTestObject(string nodeId, bool foundationRoot = false)
+ {
+ CAEXDocument document = GetDocument();
+ string rootName = TestHelper.GetRootName();
+ if ( foundationRoot )
+ {
+ rootName = TestHelper.GetOpcRootName();
+ }
+ CAEXObject initialObject = document.FindByID(rootName + nodeId);
+ Assert.IsNotNull(initialObject, "Unable to find Initial Object");
+ SystemUnitClassType theObject = initialObject as SystemUnitClassType;
+ Assert.IsNotNull(theObject, "Unable to Cast Initial Object");
+ return theObject;
+ }
+
+ #endregion
+ }
+}