Sunday 30 September 2012

Progress on unit testing

A quick update on unit testing is in order: of the 100 unit tests in the 4.7.2 solution, 79 are now passing. 5 Fail. And, a further 16 are throwing errors.

Here is a brief overview of what I have done to get tests passing. In most cases, we are dealing with the absence of the HttpContext or Application Domain values that are normally available during the application run time. I won't repeat what I have said earlier in the post, but I am hoping that there is enough stuff here to point the interested reader in the right direction. It is also important that I have gone for quick fixes, and in some instances these come with caveats, such as subtly altering application behaviour.
//umbraco.Test/SetUpUtilities.cs
using System;
using System.Collections.Specialized;
using System.Xml;
using System.Web;
using System.Web.Caching;

using umbraco.BusinessLogic;

namespace umbraco.Test
{
public class SetUpUtilities
{
public SetUpUtilities () {}

private const string _umbracoDbDSN = "server=127.0.0.1;database=UMBRACO_DB;user id=USER_ID;password=PASSWORD;datalayer=MySql";
private const string _umbracoConfigFile = "<PATH-TO-APPLICATION>/config/umbracoSettings.config";
private const string _dynamicBase = "<PATH-TO-ASSEMBLY-CACHE> (e.g., /tmp/USER_ID-temp-aspnet-0)";
public static NameValueCollection GetAppSettings()
{
NameValueCollection appSettings = new NameValueCollection();

//add application settings
appSettings.Add("umbracoDbDSN", _umbracoDbDSN);

return appSettings;
}

public static void AddUmbracoConfigFileToHttpCache()
{
XmlDocument temp = new XmlDocument();
XmlTextReader settingsReader = new XmlTextReader(_umbracoConfigFile);

temp.Load(settingsReader);
HttpRuntime.Cache.Insert("umbracoSettingsFile", temp,
new CacheDependency(_umbracoConfigFile));
}

public static void RemoveUmbracoConfigFileFromHttpCache()
{
HttpRuntime.Cache.Remove("umbracoSettingsFile");
}

public static void InitConfigurationManager()
{
ConfigurationManagerService.ConfigManager = new ConfigurationManagerTest(SetUpUtilities.GetAppSettings());
}

public static void InitAppDomainDynamicBase()
{
AppDomain.CurrentDomain.SetDynamicBase(_dynamicBase); //(Obsolete but works...)
//AppDomain.CurrentDomain.SetupInformation.DynamicBase = _dynamicBase;
}

}
}

//sample test file set-up

...
private User m_User;

[TestFixtureSetUp]
public void InitTestFixture()
{
SetUpUtilities.InitConfigurationManager();
m_User = new User(0);
SetUpUtilities.InitAppDomainDynamicBase();
}

[SetUp]
public void MyTestInitialize()
{
SetUpUtilities.AddUmbracoConfigFileToHttpCache();
...
}

[TearDown]
public void MyTestCleanup()
{
...
SetUpUtilities.RemoveUmbracoConfigFileFromHttpCache();
}

...

//couple of hacks...
.../umbraco/businesslogic/IO/MultiPlatformHelper.cs,
public static string MapUnixPath(string path)
{
string filePath = path;

if (filePath.StartsWith("~"))
filePath = IOHelper.ResolveUrl(filePath);

filePath = IOHelper.MapPath(filePath, System.Web.HttpContext.Current != null);

return filePath;
}

.../umbraco/businesslogic/IO/IOHelper.cs,
private static string getRootDirectorySafe()
{
if (!String.IsNullOrEmpty(m_rootDir))
{
return m_rootDir;
}

string baseDirectory =
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Substring(8));
m_rootDir = baseDirectory.Substring(0, baseDirectory.LastIndexOf("bin") - 1);

//changed for tests ck, 9/9/12
if (MultiPlatformHelper.IsUnix && !m_rootDir.StartsWith(IOHelper.DirSepChar.ToString()))
m_rootDir = IOHelper.DirSepChar + m_rootDir;

return m_rootDir;

}

Language_Get_By_Culture_Code terminates with error:
linq operation is not valid due to the current state of the object
do:
Language.cs (161) Replace SingleOrDefault() with FirstOrDefault()

templates, stylesheet tests fail: create directories (masterpages, css) in test project.

Saturday 8 September 2012

Issues relating to XML Caching - 2

After the previous fix, xml cache is still not properly refreshed. We now have after publish, one node but non @isDoc children are duplicated.
The xml going into the XML Cache file is corrupted.
Look at TransferValuesFromDocumentXmlToPublishedXml (323)
For some reason, this loop does not loop through all elements.
Change from this (328:329):


	foreach (XmlNode n in PublishedNode.SelectNodes(xpath))
PublishedNode.RemoveChild(n);
To:
XmlNode[] NodesToRemove =
(new List(PublishedNode.SelectNodes(xpath).OfType())).ToArray();
for (int i = 0; i < NodesToRemove.Length; i++)
PublishedNode.RemoveChild(NodesToRemove[i]);

This uses Linq, and we will need to add this as well: using System.Linq; (+ a reference to System.Core)


I have fixed this throughout the code but this would need to be tested.


content.cs 451:452
from
foreach (XmlNode child in parentNode.SelectNodes(xpath))
parentNode.RemoveChild(child);
to
XmlNode[] NodesToRemove =
(new List(parentNode.SelectNodes(xpath).OfType())).ToArray();
for (int i = 0; i < NodesToRemove.Length; i++)
parentNode.RemoveChild(NodesToRemove[i]);

macro.cs 953:954
from
foreach (XmlNode n in currentNode.SelectNodes("./node"))
currentNode.RemoveChild(n);
to
XmlNode[] NodesToRemove =
(new List(currentNode.SelectNodes("./node").OfType())).ToArray();
for (int i = 0; i < NodesToRemove.Length; i++)
currentNode.RemoveChild(NodesToRemove[i]);

StandardPackageActions.cs 493:500
from
foreach (XmlNode ext in xn.SelectNodes("//ext"))
{
if (ext.Attributes["alias"] != null && ext.Attributes["alias"].Value == _alias)
{
xn.RemoveChild(ext);
inserted = true;
}
}
to
XmlNode[] NodesToRemove =
(new List(xn.SelectNodes("//ext").OfType())).ToArray();
for (int j = 0; j < NodesToRemove.Length; j++)
{
if (NodesToRemove[j].Attributes["alias"] != null && NodesToRemove[j].Attributes["alias"].Value == _alias)
{
xn.RemoveChild(NodesToRemove[j]);
inserted = true;
}
}

StandardPackageActions.cs 588:595
from
foreach (XmlNode node in xn.SelectNodes("//allow"))
{
if (node.Attributes["host"] != null && node.Attributes["host"].Value == hostname)
{
xn.RemoveChild(node);
inserted = true;
}
}
to
XmlNode[] NodesToRemove =
(new List(xn.SelectNodes("//allow").OfType())).ToArray();
for (int j = 0; j < NodesToRemove.Length; j++)
{
if (NodesToRemove[j].Attributes["host"] != null && NodesToRemove[j].Attributes["host"].Value == hostname)
{
xn.RemoveChild(NodesToRemove[j]);
inserted = true;
}
}

Document.cs 1446:1447
from
foreach (XmlNode xDel in x.SelectNodes("./data"))
x.RemoveChild(xDel);
to
XmlNode[] NodesToRemove =
(new List(x.SelectNodes("./data").OfType())).ToArray();
for (int i = 0; i < NodesToRemove.Length; i++)
x.RemoveChild(NodesToRemove[i]);

 

Friday 7 September 2012

Issues relating to XML Caching - 1

After publish xml cache is not properly refreshed. The same doctype element is re-added, and for example, an XSLT Menu macro shows too many pages.
.../editContent.aspx.cs (315)
.../umbraco/presentation/content/content.cs line 392 calls GetElementById
-> attr.attr.OwnerElement.IsRooted (604)
-> XmlLinkedNode (52) returns false for the published document.
This is a mono issue. There is no IsRooted in the MS .net documentation.
In content.cs change
393 from,


	XmlNode x = xmlContentCopy.GetElementById(id.ToString())
to,
//Deal with IsRooted being false in mono for the published node
string xpathId = UmbracoSettings.UseLegacyXmlSchema ?
String.Format ("//node[@id = '{0}'], id.ToString()") :
String.Format ("//*[@isDoc][@id='{0}']", id.ToString());
XmlNode x = xmlContentCopy.SelectSingleNode(xpathId);

(Not so sure about the legacy syntax...) & did not vet load balancing. As far as I can tell, there is an 'IsRooted' property on Xml documents, which is set to false during the operations in getPreviewOrPublishNode(...), and the consequent call to GetElementById in AppendDocumentXml (content.cs, 393) returns null and breaks the code. That's why we do not use XmlNode x = xmlContentCopy.GetElementById(id.ToString()) here.


 

Sunday 2 September 2012

Some Media related issues

Insert image through RTE: System.NotSupportedException The type System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] is not supported because it implements IDictionary
Description: HTTP 500.Error processing request.
Details: Non-web exception. Exception origin (name of application or object): System.Xml. http://127.0.0.1:8080/umbraco/controls/Images/ImageViewerUpdater.asmx/UpdateImage at System.Xml.Serialization.TypeData.get_ListItemType () [0x000cf] in /home/kol3/Development/mono/mcs/class/System.XML/System.Xml.Serialization/TypeData.cs:331
This error can be traced to .../umbraco/presentation/umbraco/controls/images/imageViewer.ascx.cs line 96,
which calls umbraco.IO.IOHelper.ResolveUrl(...) in .../umbraco/businesslogic/IO/IOHelper.cs 44:50
In line 49, note the call to: VirtualPathUtility.ToAbsolute(string virtualPath, string AppPath).
In the mono implementation AppPath cannot be null.
We resolve as follows: .../umbraco/businesslogic/IO/MultiplatformHelper.cs, add


		public static string EnsureRootAppPath (string path)
{
if (IsUnix && (path == String.Empty))
return "/";
return path;
}

Then in .../umbraco/businesslogic/IO/IOHelper.cs, change line 49 from,


                return VirtualPathUtility.ToAbsolute(virtualPath, SystemDirectories.Root);

to


                return VirtualPathUtility.ToAbsolute(virtualPath, MultiPlatformHelper.EnsureRootAppPath(SystemDirectories.Root));

While we are at it we'll also refactor in as follows (and update all references as needed)


		public static bool IsWindows
{
get
{
return OSPlatform.Contains(PLATFORM_WIN);
}
}

public static bool IsUnix
{
get
{
return OSPlatform.Contains(PLATFORM_UNIX);
}
}

Upload png (any image) media: uploaded but not correct format error issued.
In .../umbraco/presentation/umbraco/controls/ContentControl.cs, change line 447
from


if (p.PropertyType.ValidationRegExp != "")

to


if (p.PropertyType.ValidationRegExp != null && p.PropertyType.ValidationRegExp != "")

The conditional should work, but mono is converting "" to null, and this casues the conditional to fail and give a false error message.


 

Saturday 1 September 2012

Fixing bugs, which I introduced with the 'MoveNext' issue fixes

Last week's MoveNext() fix actually turns out to be too restrictive and yielded a subtle bug.


Symptom:


Razor Code of this format:


    @{
var image = @Model.Media("relatedMedia");
}
<<mg src="http://mce_host/forum/.../@image.UmbracoFile" alt="@image.Name" />

only returns the media path with first application launch. Subsequent calls return null. I introduced this as a bug in the last set up 'MoveNext issue' fixes.


In  ExamineBackedMedia.cs change
48:51, from,
if (media != null)
{
if (media.MoveNext())
return new ExamineBackedMedia(media.Current);
to
if (media != null)
{
media.MoveNext();
if (media.Current != null)
return new ExamineBackedMedia(media.Current);

This is because the relevant XPathNodeIterator is cached, and once the index is advanced its state is retained.


I am also going to relax the remaining restrictions involving MoveNext as follows:


Again in  ExamineBackedMedia.cs change:
64:67, from,
XPathNodeIterator xpi = xpath.Select(".");
//add the attributes e.g. id, parentId etc
if (xpi.MoveNext())
if (xpi.Current.HasAttributes)
to
XPathNodeIterator xpi = xpath.Select(".");
//add the attributes e.g. id, parentId etc
xpi.MoveNext ();
if (xpi.Current != null)
if (xpi.Current.HasAttributes)

110:113 from,
var media = umbraco.library.GetMedia(this.Id, true);
if (media != null && media.MoveNext())
{
XPathNavigator xpath = media.Current;
...
to
var media = umbraco.library.GetMedia(this.Id, true);
if (media != null)
{
media.MoveNext();
if (media.Current != null)
{
XPathNavigator xpath = media.Current;
var result = xpath.SelectChildren(XPathNodeType.Element);
while (result.MoveNext())
{
if (result.Current != null && !result.Current.HasAttributes)
{
if (string.Equals(result.Current.Name, alias))
{
string value = result.Current.Value;
if (string.IsNullOrEmpty(value))
{
value = result.Current.OuterXml;
}
Values.Add(result.Current.Name, value);
propertyExists = true;
return new PropertyResult(alias, value, Guid.Empty);
}
}
}
}
}

370:373, from,
var media = umbraco.library.GetMedia(ParentId, true);
if (media != null && media.MoveNext())
{
var children = media.Current.SelectChildren(XPathNodeType.Element);
...
to
if (media != null)
{
media.MoveNext();
if (media.Current != null)
{
var children = media.Current.SelectChildren(XPathNodeType.Element);
List mediaList = new List();
while (children != null && children.Current != null)
{
if (children.MoveNext())
{
if (children.Current.Name != "contents")
{
//make sure it's actually a node, not a property
if (!string.IsNullOrEmpty(children.Current.GetAttribute("path", "")) &&
!string.IsNullOrEmpty(children.Current.GetAttribute("id", "")) &&
!string.IsNullOrEmpty(children.Current.GetAttribute("version", "")))
{
mediaList.Add(new ExamineBackedMedia(children.Current));
}
}
}
else
{
break;
}
}
return mediaList;
}
}

DynamicXml.cs, 31:33
from,
if (xpni != null)
{
if (xpni.MoveNext())
{
var xml = xpni.Current.OuterXml;
to
if (xpni != null)
{
xpni.MoveNext();
if (xpni.Current != null)
{
var xml = xpni.Current.OuterXml;


 


Also change function GetCurrentNodeFromIterator in .../umbraco/businesslogic/xmlHelper.cs to:


		//this is very mono specific at the moment
public static XmlNode GetCurrentNodeFromIterator(XPathNodeIterator xpi)
{
if (xpi != null)
{
xpi.MoveNext();
if (xpi.Current != null)
return ((IHasXmlNode)xpi.Current).GetNode();
}

return null;
}