Tom Clarkson is a SharePoint consultant and entrepreneur based in Sydney, Australia.

Contact Details

Links



Recent Searches



Archives




Past Posts







RSS Feed

Adding Test Failures and TODO Comments to RedMine

.NET
Thursday January 28 2010
Over the last few days I've been trying out Redmine as a replacement for Basecamp, which I have been using for the past year or so. Redmine has a new (and more or less undocumented) REST API for working with issues, which opens up some interesting possibilities for connecting to other systems.

Something I've been wanting to try for a while is better handling of test related tasks and todo comments, which is where the code below comes in - everything ends up in the issue tracker and can be assigned/prioritised as for regular issues.

I have TeamCity set up to build the project and produce an xml test report whenever code changes in subversion. When the test report changes, the sync app gets all tasks of the appropriate type from Redmine (I set up two new trackers, "Test" and "Todo"), compares them to what is found in the code and test report, then updates Redmine as needed.

Note that this is proof of concept, works-on-my-machine code - It shouldn't be too hard to make it do what you want, but there are no guarantees.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Net;
using System.Xml;

namespace OrangeGuava.TaskSync
{
    enum TaskType
    {
        Test,
        Todo
    }

    class Task
    {
        public int? Id { get; set; }
        public TaskType Type { get; set; }
        public string Filename { get; set; }
        public string Result { get; set; }
        public int Line { get; set; }
        public string Text { get; set; }
        public int Status { get; set; }
    }

    class Program
    {

        static int TRACKER_TODO = 5;
        static int TRACKER_TEST = 4;
        static string ServerUrl = "http://redmineURL";
        static int ProjectId = 1;
        static string basepath = //@"C:\code\twoneeds\";
        @"C:\TeamCity\buildAgent\work\597b948abdcc6ce0\";
        static string login = "TaskSync";
        static string password = "[password]";


        static void UpdateTask(Task t)
        {

            var request = (HttpWebRequest)HttpWebRequest.Create(
                string.Format(
                "{0}/issues/{1}.xml", ServerUrl, t.Id));

            string cre = String.Format("{0}:{1}", login, password);
            byte[] bytes = Encoding.ASCII.GetBytes(cre);
            string base64 = Convert.ToBase64String(bytes);
            request.Headers.Add("Authorization", "Basic " + base64);
            request.ContentType = "application/xml";

            request.Method = "PUT";

            var reqsw = new StreamWriter(request.GetRequestStream());

            WriteTask(reqsw, t);

            reqsw.Close();

            var response = request.GetResponse();
            response.Close();


        }

        static void WriteTask(StreamWriter reqsw, Task t)
        {
            XmlDocument doc = new XmlDocument();
            doc.AppendChild(doc.CreateElement("issue"));
            doc.DocumentElement.SetAttribute("project_id", ProjectId.ToString());
            doc.DocumentElement.SetAttribute("tracker_id", (t.Type == TaskType.Test ? TRACKER_TEST : TRACKER_TODO).ToString());
            doc.DocumentElement.SetAttribute("status_id", t.Status.ToString());

            if (t.Type == TaskType.Todo)
            {
                var fn = t.Filename;
                if (fn.Contains('/')) fn = fn.Substring(fn.LastIndexOf('/'));

                var subj = doc.CreateElement("subject");
                doc.DocumentElement.AppendChild(subj);
                subj.InnerText = fn + ":" + t.Line + " - " + t.Text;

                var desc = doc.CreateElement("description");
                doc.DocumentElement.AppendChild(desc);
                desc.InnerText = t.Filename + "\n" + t.Line + "\n" + t.Text;
            }
            else
            {

                var subj = doc.CreateElement("subject");
                doc.DocumentElement.AppendChild(subj);
                subj.InnerText = t.Result + ": " + t.Filename;

                var desc = doc.CreateElement("description");
                doc.DocumentElement.AppendChild(desc);
                desc.InnerText = t.Filename + "\n" + t.Result + "\n" + t.Text;
            }

            reqsw.Write(doc.DocumentElement.OuterXml);

        }

        static void CreateTask(Task t) {

            var request = (HttpWebRequest)HttpWebRequest.Create(
                string.Format(
                "{0}/issues.xml", ServerUrl));

            string cre = String.Format("{0}:{1}", login, password);
            byte[] bytes = Encoding.ASCII.GetBytes(cre);
            string base64 = Convert.ToBase64String(bytes);
            request.Headers.Add("Authorization", "Basic " + base64);
            request.ContentType= "application/xml";

            request.Method = "POST";
            
            var reqsw = new StreamWriter(request.GetRequestStream());

            WriteTask(reqsw, t);

            reqsw.Close();
            
            var response = request.GetResponse();
            response.Close();

        }

        static List GetTasksFromIssueTracker(TaskType type, int page)
        {
            var result = new List();

            var request = (HttpWebRequest)HttpWebRequest.Create(
                string.Format(
                "{0}/issues.xml?project_id={1}&tracker_id={2}&status_id=*&page={3}", 
                ServerUrl, 
                ProjectId,
                type == TaskType.Test ? TRACKER_TEST : TRACKER_TODO,
                page
                ));
            //request.Credentials = new NetworkCredential("tqc01", "asdf12");
            string cre = String.Format("{0}:{1}", login, password);
            byte[] bytes = Encoding.ASCII.GetBytes(cre);
            string base64 = Convert.ToBase64String(bytes);
            request.Headers.Add("Authorization", "Basic " + base64);

            var response = (HttpWebResponse)request.GetResponse();

            StreamReader reader = new StreamReader(response.GetResponseStream());
            string xml = reader.ReadToEnd();
            response.Close();

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            var total = int.Parse(doc.DocumentElement.GetAttribute("count"));
            var pages = (int)Math.Ceiling(total / 25.0);

            var issueNodes = doc.DocumentElement.SelectNodes("//issue");

            foreach (XmlElement issueNode in issueNodes)
            {
                var task = new Task();
                task.Type = type;
                task.Id = int.Parse(issueNode.SelectSingleNode("./id").InnerText);
                task.Status = int.Parse(((XmlElement)issueNode.SelectSingleNode("./status")).GetAttribute("id"));
                //task.Text = issueNode.SelectSingleNode("./subject").InnerText;
                var description = issueNode.SelectSingleNode("./description").InnerText;
                var ds = description.Split('\n');
                if (type == TaskType.Todo)
                {
                    task.Filename = ds[0];
                    task.Line = int.Parse(ds[1]);
                    task.Text = (ds[2]);
                }

                if (type == TaskType.Test)
                {
                    task.Filename = ds[0].Trim();
                    task.Result = (ds[1]).Trim();
                    task.Text = description.Substring(description.IndexOf('\n', description.IndexOf('\n')+1) + 1);
                }
                result.Add(task);
            }


            if (page < pages)
            {
                result.AddRange(GetTasksFromIssueTracker(type, page + 1));
            }
            return result;
        }

        static List GetTasksFromCode()
        {
            var result = new List();
            

            var files = Directory.GetFiles(basepath, "*.cs", SearchOption.AllDirectories);

            foreach (var fn in files)
            {
                //Console.WriteLine(fn);
                var lines = File.ReadAllLines(fn);
                for (int i = 0; i < lines.Length; i++)
                {
                    var l = lines[i];
                    var m = Regex.Match(l, "//.*TODO:(.*)$", RegexOptions.IgnoreCase);
                    if (m.Success)
                    {
                        result.Add(new Task()
                        {
                            Type = TaskType.Todo,
                            Filename = fn.Substring(basepath.Length),
                            Line = i,
                            Text = m.Groups[1].Value.Trim(),
                            Status = 1
                        });
                    }
                }
            }
            return result;
        }

        static void SyncTests(string path)
        {
            var oldtasks = GetTasksFromIssueTracker(TaskType.Test, 1);
            var newtasks = GetTasksFromTestRecord(path);


            Console.WriteLine("Server: " + oldtasks.Count);
            Console.WriteLine("Code: " + newtasks.Count);


            // updates - either no change, status change, or no longer there

            foreach (var ts in oldtasks.ToArray())
            {
                bool found = false;
                foreach (var tt in newtasks)
                {
                    if (tt.Filename == ts.Filename)
                    {
                        // test matches
                        found = true;
                        if (tt.Result == ts.Result && Regex.Replace(tt.Text, "\\W", "") == Regex.Replace(ts.Text, "\\W", ""))
                        {
                            // no change
                            oldtasks.Remove(ts);
                            newtasks.Remove(tt);
                        }
                        else
                        {
                            // update required
                            ts.Result = tt.Result;
                            ts.Text = tt.Text;
                            if (ts.Status == 5) ts.Status = 1;
                            newtasks.Remove(tt);
                        }
                        break;

                    }

                    if (!found)
                    {
                        // test passed or removed
                        ts.Status = 5;
                    }
                }
            }

            Console.WriteLine("New: " + newtasks.Count);
            Console.WriteLine("Updated: " + oldtasks.Count);


            // save updates
            foreach (var t in oldtasks)
            {
                Console.WriteLine("Updating " + t.Filename);
                UpdateTask(t);
            }

            // create new tasks
            foreach (var t in newtasks)
            {
                Console.WriteLine("Adding " + t.Filename);
                CreateTask(t);
            }


        }

        private static List GetTasksFromTestRecord(string path)
        {
            var result = new List();
            XmlDocument doc = new XmlDocument();
            doc.Load(path);
            var testnodes = doc.SelectNodes("//test-case");

            foreach (XmlElement testnode in testnodes)
            {
                if (testnode.GetAttribute("executed") == "False")
                {
                    // ignored
                    result.Add(new Task()
                    {
                        Type = TaskType.Test,
                        Filename = testnode.GetAttribute("name"),
                        Result = "Ignored",
                        Status = 1,
                        Text = testnode.SelectSingleNode("./reason").InnerText                        
                    });
                }
                else if (testnode.GetAttribute("success") == "False")
                {
                    // failed
                    result.Add(new Task()
                    {
                        Type = TaskType.Test,
                        Filename = testnode.GetAttribute("name"),
                        Result = "Failed",
                        Status = 1,
                        Text = testnode.SelectSingleNode("./failure").InnerText
                    });
                }
            }
            return result;
        }


        static void SyncToDo()
        {

            var oldtasks = GetTasksFromIssueTracker(TaskType.Todo, 1);
            var newtasks = GetTasksFromCode();
       

            Console.WriteLine("Server: " + oldtasks.Count);
            Console.WriteLine("Code: " + newtasks.Count);

            // ignore exact matches

            foreach (var ts in oldtasks.ToArray())
            {
                foreach (var tc in newtasks)
                {
                    if (ts.Filename == tc.Filename && ts.Line == tc.Line && ts.Text == tc.Text)
                    {
                        // exact match
                        newtasks.Remove(tc);
                        oldtasks.Remove(ts);
                        break;
                    }
                }
            }

            var updates = new List();

            // find moved tasks
            foreach (var ts in oldtasks.ToArray())
            {
                foreach (var tc in newtasks)
                {
                    if (ts.Filename == tc.Filename && ts.Line != tc.Line && ts.Text == tc.Text)
                    {
                        // line changed
                        newtasks.Remove(tc);
                        oldtasks.Remove(ts);
                        ts.Line = tc.Line;
                        updates.Add(ts);
                        break;
                    }
                }
            }

            // find edited tasks
            foreach (var ts in oldtasks.ToArray())
            {
                foreach (var tc in newtasks)
                {
                    if (ts.Filename == tc.Filename && ts.Line == tc.Line && ts.Text != tc.Text)
                    {
                        newtasks.Remove(tc);
                        oldtasks.Remove(ts);
                        ts.Text = tc.Text;
                        updates.Add(ts);
                        break;
                    }
                }
                //                Console.WriteLine(t.Text);
                //   CreateTask(t);                               
            }

            Console.WriteLine("Completed: " + oldtasks.Count);
            Console.WriteLine("New: " + newtasks.Count);
            Console.WriteLine("Updated: " + updates.Count);


            // save updates
            foreach (var t in updates)
            {
                Console.WriteLine("Updating " + t.Text);
                UpdateTask(t);
            }

            // mark old tasks complete
            foreach (var t in oldtasks)
            {
                if (t.Status == 5) continue;
                Console.WriteLine("Closing " + t.Text);
                t.Status = 5;
                UpdateTask(t);
            }


            // create new tasks
            foreach (var t in newtasks)
            {
                Console.WriteLine("Adding " + t.Text);
                CreateTask(t);
            }

        }


        static DateTime lastchange = DateTime.MinValue;

        static void Main(string[] args)
        {

            FileSystemWatcher fsw = new FileSystemWatcher(basepath);
            fsw.Changed += new FileSystemEventHandler(fsw_Changed);
            fsw.Renamed += new RenamedEventHandler(fsw_Changed);
            fsw.Created += new FileSystemEventHandler(fsw_Changed);
            fsw.EnableRaisingEvents = true;

            Console.WriteLine("Waiting for change on "+basepath);
            Console.ReadLine();
        }

        
        static void fsw_Changed(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(e.FullPath);
            if (!e.FullPath.ToLower().EndsWith("orangeguava.onlineservice.unittests.dll-results.xml")) return;
            if (lastchange > DateTime.Now.AddSeconds(-30)) return;
            lastchange = DateTime.Now;
            
            Console.WriteLine("Changed");
            SyncTests(e.FullPath);
            SyncToDo();
            Console.WriteLine("Sync complete - waiting for change");
        }
    }
}

Comments

Leave a comment