chiark / gitweb /
windows: job queue class & associated tests
authorRichard Kettlewell <rjk@terraraq.org.uk>
Sat, 22 Dec 2012 11:33:13 +0000 (11:33 +0000)
committerRichard Kettlewell <rjk@terraraq.org.uk>
Sat, 22 Dec 2012 11:33:13 +0000 (11:33 +0000)
.gitignore
mandy.sln
windows/mandycs/JobQueue.cs [new file with mode: 0644]
windows/mandycs/Properties/AssemblyInfo.cs [new file with mode: 0644]
windows/mandycs/mandycs.csproj [new file with mode: 0644]
windows/tests/JobQueueTest.cs [new file with mode: 0644]
windows/tests/Properties/AssemblyInfo.cs [new file with mode: 0644]
windows/tests/tests.csproj [new file with mode: 0644]

index 240ca14..c85205a 100644 (file)
@@ -37,3 +37,4 @@ GRTAGS
 GSYMS
 GTAGS
 ipch
+TestResults
index 8e7f404..ab8eedb 100644 (file)
--- a/mandy.sln
+++ b/mandy.sln
@@ -18,64 +18,110 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fixed128-test", "windows\fi
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D} = {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}
        EndProjectSection
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mandycs", "windows\mandycs\mandycs.csproj", "{BF145F89-A268-4619-AAFD-02D63B2685D2}"
+       ProjectSection(ProjectDependencies) = postProject
+               {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D} = {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}
+       EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "windows\tests\tests.csproj", "{9EABF08C-7632-49B2-96FD-D93C623205CA}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
+               Debug|Any CPU = Debug|Any CPU
                Debug|Mixed Platforms = Debug|Mixed Platforms
                Debug|Win32 = Debug|Win32
                Debug|x64 = Debug|x64
+               Release|Any CPU = Release|Any CPU
                Release|Mixed Platforms = Release|Mixed Platforms
                Release|Win32 = Release|Win32
                Release|x64 = Release|x64
        EndGlobalSection
        GlobalSection(ProjectConfigurationPlatforms) = postSolution
+               {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|Any CPU.ActiveCfg = Debug|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|Mixed Platforms.Build.0 = Debug|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|Win32.ActiveCfg = Debug|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|Win32.Build.0 = Debug|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|x64.ActiveCfg = Debug|x64
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Debug|x64.Build.0 = Debug|x64
+               {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|Any CPU.ActiveCfg = Release|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|Mixed Platforms.ActiveCfg = Release|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|Mixed Platforms.Build.0 = Release|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|Win32.ActiveCfg = Release|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|Win32.Build.0 = Release|Win32
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|x64.ActiveCfg = Release|x64
                {E5DDECA6-1F6C-401B-BBB6-1101B1F5B61D}.Release|x64.Build.0 = Release|x64
+               {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|Any CPU.ActiveCfg = Debug|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|Mixed Platforms.Build.0 = Debug|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|Win32.ActiveCfg = Debug|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|Win32.Build.0 = Debug|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|x64.ActiveCfg = Debug|x64
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Debug|x64.Build.0 = Debug|x64
+               {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|Any CPU.ActiveCfg = Release|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|Mixed Platforms.ActiveCfg = Release|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|Mixed Platforms.Build.0 = Release|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|Win32.ActiveCfg = Release|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|Win32.Build.0 = Release|Win32
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|x64.ActiveCfg = Release|x64
                {15F60354-A2F4-4C39-91E1-656DA501CE79}.Release|x64.Build.0 = Release|x64
+               {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|Any CPU.ActiveCfg = Debug|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|Mixed Platforms.Build.0 = Debug|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|Win32.ActiveCfg = Debug|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|Win32.Build.0 = Debug|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|x64.ActiveCfg = Debug|x64
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Debug|x64.Build.0 = Debug|x64
+               {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|Any CPU.ActiveCfg = Release|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|Mixed Platforms.ActiveCfg = Release|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|Mixed Platforms.Build.0 = Release|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|Win32.ActiveCfg = Release|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|Win32.Build.0 = Release|Win32
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|x64.ActiveCfg = Release|x64
                {8A51A96E-3178-4E07-8ADC-31613CFFC2BD}.Release|x64.Build.0 = Release|x64
+               {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|Any CPU.ActiveCfg = Debug|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|Mixed Platforms.Build.0 = Debug|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|Win32.ActiveCfg = Debug|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|Win32.Build.0 = Debug|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|x64.ActiveCfg = Debug|x64
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Debug|x64.Build.0 = Debug|x64
+               {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|Any CPU.ActiveCfg = Release|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|Mixed Platforms.ActiveCfg = Release|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|Mixed Platforms.Build.0 = Release|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|Win32.ActiveCfg = Release|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|Win32.Build.0 = Release|Win32
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|x64.ActiveCfg = Release|x64
                {51AC3A69-21F2-4E24-B5AD-289F1EE8FCFE}.Release|x64.Build.0 = Release|x64
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Win32.ActiveCfg = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|Win32.Build.0 = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Debug|x64.Build.0 = Debug|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|Any CPU.Build.0 = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|Win32.ActiveCfg = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|x64.ActiveCfg = Release|Any CPU
+               {BF145F89-A268-4619-AAFD-02D63B2685D2}.Release|x64.Build.0 = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Win32.ActiveCfg = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|Win32.Build.0 = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Debug|x64.Build.0 = Debug|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|Any CPU.Build.0 = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|Win32.ActiveCfg = Release|Any CPU
+               {9EABF08C-7632-49B2-96FD-D93C623205CA}.Release|x64.ActiveCfg = Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
diff --git a/windows/mandycs/JobQueue.cs b/windows/mandycs/JobQueue.cs
new file mode 100644 (file)
index 0000000..6646792
--- /dev/null
@@ -0,0 +1,252 @@
+\feffusing System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace uk.org.greenend.mandy
+{
+  /// <summary>
+  /// A job in the job queue
+  /// </summary>
+  public class Job
+  {
+    /// <summary>
+    /// Context object (for cancellation)
+    /// </summary>
+    internal object context;
+
+    /// <summary>
+    /// Called in a background thread to run the job.
+    /// </summary>
+    /// <remarks>The default implementation does nothing.</remarks>
+    public virtual void Run()
+    {
+    }
+
+    /// <summary>
+    /// Called from JobQueue.Complete to complete the job.
+    /// </summary>
+    /// <remarks>The default implementation does nothing.</remarks>
+    public virtual void Complete()
+    {
+    }
+
+    /// <summary>
+    /// Called if a job is cancelled without being run
+    /// </summary>
+    /// <remarks>The default implementation does nothing.</remarks>
+    public virtual void Cancel()
+    {
+    }
+  }
+
+  /// <summary>
+  /// The job queue
+  /// </summary>
+  public static class JobQueue
+  {
+    /// <summary>
+    /// Target number of threads
+    /// </summary>
+    /// <remarks>
+    /// <para>Don't change this after adding any jobs - it will be ignored.</para>
+    /// <para>The default is the number of logical CPUs.</para>
+    /// </remarks>
+    public static int Workers = Environment.ProcessorCount;
+
+    /// <summary>
+    /// Set to true when worker threads have been created.
+    /// </summary>
+    private static bool workersCreated = false;
+
+    /// <summary>
+    /// Queued jobs
+    /// </summary>
+    private static LinkedList<Job> jobsPending = new LinkedList<Job>();
+
+    /// <summary>
+    /// Jobs currently being processed
+    /// </summary>
+    private static HashSet<Job> jobsWorking = new HashSet<Job>();
+
+    /// <summary>
+    /// Completed jobs awaiting collection
+    /// </summary>
+    private static LinkedList<Job> jobsComplete = new LinkedList<Job>();
+
+    /// <summary>
+    /// Worker thread
+    /// </summary>
+    static private void Worker()
+    {
+      Monitor.Enter(jobsPending);
+      while (true)
+      {
+        if (jobsPending.Count > 0)
+        {
+          // There is work to be done
+          Job j = jobsPending.First.Value;
+          jobsPending.RemoveFirst();
+          jobsWorking.Add(j);
+          Monitor.Exit(jobsPending);
+          try
+          {
+            j.Run();
+          }
+          catch (Exception)
+          {
+            // Jobs that throw exceptions just lose them.
+          }
+          lock (jobsComplete)
+          {
+            jobsComplete.AddLast(j);
+            Monitor.PulseAll(jobsComplete);
+          }
+          // NB that a job can be in both jobsWorking and jobsComplete
+          // at the same time.
+          Monitor.Enter(jobsPending);
+          jobsWorking.Remove(j);
+          continue;
+        }
+        // If we didn't find anything to do, wait
+        Monitor.Wait(jobsPending);
+      }
+      Monitor.Exit(jobsPending);
+    }
+
+    /// <summary>
+    /// Create a new worker thread
+    /// </summary>
+    /// <remarks><para>Called with lock held.</para></remarks>
+    static private void CreateWorkers()
+    {
+      for (int n = 0; n < Workers; ++n)
+      {
+        Thread newThread = new Thread(new ThreadStart(Worker))
+        {
+          IsBackground = true
+        };
+        newThread.Start();
+      }
+      workersCreated = true;
+    }
+    
+    /// <summary>
+    /// Add a job to the queue
+    /// </summary>
+    /// <param name="job">Job to enqueue</param>
+    /// <param name="context">Context information</param>
+    public static void Add(Job job, object context)
+    {
+      lock (jobsPending)
+      {
+        if (!workersCreated)
+        {
+          CreateWorkers();
+        }
+        job.context = context;
+        jobsPending.AddLast(job);
+        Monitor.Pulse(jobsPending);
+      }
+    }
+
+    /// <summary>
+    /// Cancel all pending jobs for a given context
+    /// </summary>
+    /// <param name="context">Context to remove</param>
+    /// <remarks><para>Note that working and completed jobs are not removed.</para></remarks>
+    // TODO but maybe they should be - we could promise that once
+    // Cancel() has been called you'll never see anything from that
+    // context again, so it'd be a guarantee rather than an optimization.
+    public static void Cancel(object context)
+    {
+      List<Job> cancelled = new List<Job>();
+      lock (jobsPending)
+      {
+        for (var node = jobsPending.First; node != null; )
+        {
+          var next = node.Next;
+          if (node.Value.context == context)
+          {
+            jobsPending.Remove(node);
+            // The cancel callback had better be issued outside the lock
+            cancelled.Add(node.Value);
+          }
+          node = next;
+        }
+      }
+      foreach (var job in cancelled)
+      {
+        job.Cancel();
+      }
+    }
+
+    /// <summary>
+    /// Run completion for one or more completed jobs
+    /// </summary>
+    /// <param name="count">Maximum number of job completions to run</param>
+    /// <param name="context">If not null, only complete jobs in this context</param>
+    /// <param name="block">If true, block until all jobs completed, or queue empty</param>
+    /// <returns>The number of jobs that were completed.</returns>
+    /// <remarks><para>The intended use is for Complete to be restricted
+    /// to a single thread.  However, it should be safe to call it from multiple
+    /// threads, provided you don't mind your Complete() overrides running
+    /// in unpredictable threads (if context isn't set).</para></remarks>
+    public static int Complete(int count, object context, bool block)
+    {
+      // Special case a count of 0 - it's a stupid value to pass
+      // but it shouldn't cause a hang.
+      if (count == 0)
+      {
+        return 0;
+      }
+      int completed = 0;
+      Monitor.Enter(jobsComplete);
+      while (true)
+      {
+        int completedThisTime = 0;
+        for (var node = jobsComplete.First; count > 0 && node != null; )
+        {
+          var next = node.Next;
+          if (context == null || node.Value.context == context)
+          {
+            // Found a suitable job.  Remove it from the list
+            // and then call Complete() on it.
+            jobsComplete.Remove(node);
+            Monitor.Exit(jobsComplete);
+            node.Value.Complete();
+            ++completedThisTime;
+            --count;
+            Monitor.Enter(jobsComplete);
+          }
+          node = next;
+        }
+        completed += completedThisTime;
+        if (count == 0)
+        {
+          // We've done as much work as we were asked to do.
+          break;
+        }
+        if (completedThisTime > 0)
+        {
+          // If we did any work we'll have released the lock, so it's
+          // possible that more work arrived in the meantime.  Go back
+          // and check.
+          continue;
+        }
+        if (!block)
+        {
+          // We didn't find any work to do.  If we were asked not to block,
+          // exit now.
+          break;
+        }
+        // Wait for more work to arrive.
+        Monitor.Wait(jobsComplete);
+      }
+      Monitor.Exit(jobsComplete);
+      return completed;
+    }
+  }
+}
diff --git a/windows/mandycs/Properties/AssemblyInfo.cs b/windows/mandycs/Properties/AssemblyInfo.cs
new file mode 100644 (file)
index 0000000..717c8ac
--- /dev/null
@@ -0,0 +1,36 @@
+\feffusing System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("mandycs")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("mandycs")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("834b714d-c664-41f0-b9c4-418d4c5b6e0b")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/windows/mandycs/mandycs.csproj b/windows/mandycs/mandycs.csproj
new file mode 100644 (file)
index 0000000..9132a80
--- /dev/null
@@ -0,0 +1,53 @@
+\feff<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{BF145F89-A268-4619-AAFD-02D63B2685D2}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>uk.org.greenend.mandy</RootNamespace>
+    <AssemblyName>mandycs</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="JobQueue.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>
\ No newline at end of file
diff --git a/windows/tests/JobQueueTest.cs b/windows/tests/JobQueueTest.cs
new file mode 100644 (file)
index 0000000..d2a8f77
--- /dev/null
@@ -0,0 +1,105 @@
+\feffusing System;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using uk.org.greenend.mandy;
+
+namespace tests
+{
+  /// <summary>
+  /// Test job that just records what happened to it
+  /// </summary>
+  class TestJob : Job
+  {
+    public override void Run()
+    {
+      hasRun = true;
+    }
+
+    public override void Complete()
+    {
+      hasCompleted = true;
+    }
+
+    public override void Cancel()
+    {
+      hasCancelled = true;
+    }
+
+    public bool hasRun = false;
+
+    public bool hasCompleted = false;
+
+    public bool hasCancelled = false;
+  }
+
+  /// <summary>
+  /// Tests for JobQueue
+  /// </summary>
+  [TestClass]
+  public class JobQueueTest
+  {
+    /// <summary>
+    /// Test that jobs are run and completed
+    /// </summary>
+    [TestMethod]
+    public void RunJobs()
+    {
+      const int nJobs = 128;
+      List<TestJob> jobs = new List<TestJob>();
+      // Create a collection of jobs and run them
+      for (int i = 0; i < nJobs; ++i)
+      {
+        TestJob job = new TestJob();
+        jobs.Add(job);
+        JobQueue.Add(job, this);
+      }
+      int complete = JobQueue.Complete(nJobs, null, true);
+      Assert.AreEqual(nJobs, complete, string.Format("only {0} jobs completed", complete));
+      for(int i = 0; i < nJobs; ++i)
+      {
+        var job = jobs[i];
+        Assert.AreEqual(true, job.hasRun, string.Format("job {0} wasn't run", i));
+        Assert.AreEqual(true, job.hasCompleted, string.Format("job {0} wasn't completed", i));
+      }
+    }
+
+    /// <summary>
+    /// Test that cancellation works
+    /// </summary>
+    [TestMethod]
+    public void CancelJobs()
+    {
+      const int nJobs = 128;
+      List<TestJob> classOneJobs = new List<TestJob>();
+      List<TestJob> classTwoJobs = new List<TestJob>();
+      for (int i = 0; i < nJobs; ++i)
+      {
+        TestJob job = new TestJob();
+        classOneJobs.Add(job);
+        JobQueue.Add(job, classOneJobs);
+        job = new TestJob();
+        classTwoJobs.Add(job);
+        JobQueue.Add(job, classTwoJobs);
+      }
+      JobQueue.Cancel(classTwoJobs);
+      int complete = JobQueue.Complete(2 * nJobs, null, true);
+      Assert.IsTrue(complete >= nJobs, string.Format("only {0} jobs completed", complete));
+      for (int i = 0; i < nJobs; ++i)
+      {
+        var job = classOneJobs[i];
+        Assert.AreEqual(true, job.hasRun, string.Format("class one job {0} wasn't run", i));
+        Assert.AreEqual(true, job.hasCompleted, string.Format("class one job {0} wasn't completed", i));
+        job = classTwoJobs[i];
+        if (job.hasRun)
+        {
+          Assert.AreEqual(true, job.hasCompleted, string.Format("class two job {0} run but wasn't completed", i));
+        }
+        else
+        {
+          Assert.AreEqual(false, job.hasCompleted, string.Format("class two job {0} didn't run but was completed", i));
+          Assert.AreEqual(true, job.hasCancelled, string.Format("class two job {0} didn't run but wasn't cancelled", i));
+        }
+      }  
+    }
+  }
+}
diff --git a/windows/tests/Properties/AssemblyInfo.cs b/windows/tests/Properties/AssemblyInfo.cs
new file mode 100644 (file)
index 0000000..1edbc63
--- /dev/null
@@ -0,0 +1,36 @@
+\feffusing System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("tests")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("543a4df3-189a-45fd-8fac-af6f0558fd5f")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/windows/tests/tests.csproj b/windows/tests/tests.csproj
new file mode 100644 (file)
index 0000000..3d6b848
--- /dev/null
@@ -0,0 +1,89 @@
+\feff<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{9EABF08C-7632-49B2-96FD-D93C623205CA}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>tests</RootNamespace>
+    <AssemblyName>tests</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
+    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+    <ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
+    <IsCodedUITest>False</IsCodedUITest>
+    <TestProjectType>UnitTest</TestProjectType>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+  </ItemGroup>
+  <Choose>
+    <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
+      <ItemGroup>
+        <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+      </ItemGroup>
+    </When>
+    <Otherwise>
+      <ItemGroup>
+        <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
+      </ItemGroup>
+    </Otherwise>
+  </Choose>
+  <ItemGroup>
+    <Compile Include="JobQueueTest.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\mandycs\mandycs.csproj">
+      <Project>{bf145f89-a268-4619-aafd-02d63b2685d2}</Project>
+      <Name>mandycs</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Choose>
+    <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
+      <ItemGroup>
+        <Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+          <Private>False</Private>
+        </Reference>
+        <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+          <Private>False</Private>
+        </Reference>
+        <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+          <Private>False</Private>
+        </Reference>
+        <Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+          <Private>False</Private>
+        </Reference>
+      </ItemGroup>
+    </When>
+  </Choose>
+  <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>
\ No newline at end of file