Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Got basics working. just need to finish the diff

  • Loading branch information...
commit f03d14a8115d72194c7908dc19e9e07dd6fead86 0 parents
unknown authored
Showing with 11,364 additions and 0 deletions.
  1. +11 −0 .gitignore
  2. +8 −0 Teamcity.Rx.Specs/App.config
  3. +36 −0 Teamcity.Rx.Specs/Properties/AssemblyInfo.cs
  4. +79 −0 Teamcity.Rx.Specs/Teamcity.Rx.Specs.csproj
  5. +54 −0 Teamcity.Rx.Specs/TeamcityStatusObservableSpecs.cs
  6. +44 −0 Teamcity.Rx.Specs/TeamcityStatusParserSpecs.cs
  7. +10,062 −0 Teamcity.Rx.Specs/test_data/test1.html
  8. +26 −0 Teamcity.Rx.sln
  9. +19 −0 Teamcity.Rx/ActionDisposable.cs
  10. +82 −0 Teamcity.Rx/BackgroundWorkerObservable.cs
  11. +91 −0 Teamcity.Rx/Build.cs
  12. +7 −0 Teamcity.Rx/BuildState.cs
  13. +11 −0 Teamcity.Rx/EmptyDisposable.cs
  14. +27 −0 Teamcity.Rx/Events/BuildAdded.cs
  15. +35 −0 Teamcity.Rx/Events/BuildRemoved.cs
  16. +68 −0 Teamcity.Rx/Events/BuildUpdated.cs
  17. +7 −0 Teamcity.Rx/Events/ITeamcityEvent.cs
  18. +27 −0 Teamcity.Rx/Events/ProjectAdded.cs
  19. +22 −0 Teamcity.Rx/Events/ProjectRemoved.cs
  20. +53 −0 Teamcity.Rx/Parser/BuildRowParser.cs
  21. +29 −0 Teamcity.Rx/Parser/ProjectRowParser.cs
  22. +19 −0 Teamcity.Rx/Parser/RowParser.cs
  23. +37 −0 Teamcity.Rx/Parser/TeamcityStatusParseException.cs
  24. +68 −0 Teamcity.Rx/Parser/TeamcityStatusParser.cs
  25. +117 −0 Teamcity.Rx/Project.cs
  26. +37 −0 Teamcity.Rx/Properties/AssemblyInfo.cs
  27. +91 −0 Teamcity.Rx/Teamcity.Rx.csproj
  28. +60 −0 Teamcity.Rx/Teamcity.cs
  29. +59 −0 Teamcity.Rx/TeamcityEventObservable.cs
  30. +34 −0 Teamcity.Rx/TeamcityObservable.cs
  31. +44 −0 Teamcity.Rx/TeamcityStatusObservable.cs
  32. BIN  lib/Hammock.dll
  33. BIN  lib/Hammock.pdb
  34. BIN  lib/HtmlAgilityPack.dll
  35. BIN  lib/HtmlAgilityPack.pdb
  36. BIN  lib/Machine.Specifications.dll
  37. BIN  lib/Newtonsoft.Json.dll
  38. BIN  lib/System.CoreEx.dll
  39. BIN  lib/System.Reactive.dll
  40. BIN  lib/log4net.dll
11 .gitignore
@@ -0,0 +1,11 @@
+obj
+bin
+deploy
+deploy/*
+_ReSharper.*
+*.csproj.user
+*.resharper.user
+*.resharper
+*.suo
+*.cache
+~$*
8 Teamcity.Rx.Specs/App.config
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="hostname" value="http://teamcity.toptable.com"/>
+ <add key="username" value="james.hollingworth"/>
+ <add key="password" value="password"/>
+ </appSettings>
+</configuration>
36 Teamcity.Rx.Specs/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using 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("Teamcity.Rx.Specs")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Teamcity.Rx.Specs")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[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("925b772d-eec2-4632-89a6-4727bb4776d2")]
+
+// 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")]
79 Teamcity.Rx.Specs/Teamcity.Rx.Specs.csproj
@@ -0,0 +1,79 @@
+<?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>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{84D63EEB-12A8-4D94-9E63-F91B3859DDA7}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Teamcity.Rx.Specs</RootNamespace>
+ <AssemblyName>Teamcity.Rx.Specs</AssemblyName>
+ <TargetFrameworkVersion>v4.0</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="Machine.Specifications">
+ <HintPath>..\lib\Machine.Specifications.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.CoreEx">
+ <HintPath>..\lib\System.CoreEx.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive">
+ <HintPath>..\lib\System.Reactive.dll</HintPath>
+ </Reference>
+ <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="TeamcityStatusObservableSpecs.cs" />
+ <Compile Include="TeamcityStatusParserSpecs.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="test_data\test1.html">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\Teamcity.Rx\Teamcity.Rx.csproj">
+ <Project>{3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}</Project>
+ <Name>Teamcity.Rx</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.config" />
+ </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>
54 Teamcity.Rx.Specs/TeamcityStatusObservableSpecs.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Threading;
+using Machine.Specifications;
+
+namespace Teamcity.Rx.Specs
+{
+ internal class TestStatusObserver : IObserver<string>
+ {
+ public TestStatusObserver()
+ {
+ Statuses = new List<string>();
+ Errors = new List<Exception>();
+ }
+
+ public List<string> Statuses { get; set; }
+ public List<Exception> Errors { get; set; }
+
+ public void OnNext(string value)
+ {
+ Statuses.Add(value);
+ }
+
+ public void OnError(Exception error)
+ {
+ Errors.Add(error);
+ }
+
+ public void OnCompleted()
+ {
+ }
+ }
+
+ public class When_I_get_the_teamcity_status
+ {
+ static TeamcityStatusObservable StatusObservable;
+ static TestStatusObserver StatusObserver;
+
+ Establish context = () =>
+ {
+ StatusObservable = new TeamcityStatusObservable(ConfigurationManager.AppSettings["hostname"], ConfigurationManager.AppSettings["username"], ConfigurationManager.AppSettings["password"]);
+ StatusObserver = new TestStatusObserver();
+ };
+
+ Because we_observed_the_teamcity_status = () =>
+ {
+ StatusObservable.Subscribe(StatusObserver);
+ Thread.Sleep(new TimeSpan(0, 0, 15));
+ };
+
+ It should_return_some_statuses = () => StatusObserver.Statuses.Count.ShouldBeGreaterThan(0);
+ }
+}
44 Teamcity.Rx.Specs/TeamcityStatusParserSpecs.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Machine.Specifications;
+using Teamcity.Rx.Parser;
+
+namespace Teamcity.Rx.Specs
+{
+ public class TeamcityStatusParserSpecs
+ {
+ static string Html;
+ protected static List<Project> Projects;
+ protected static TeamcityStatusParser Subject;
+
+ protected static string TestDataFile
+ {
+ set
+ {
+ Html = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "test_data", value + ".html"));
+ }
+ }
+
+ protected static void parsed_the_html()
+ {
+ new TeamcityStatusParser(Html).Subscribe(projects => Projects = projects);
+ }
+
+ protected static Project the_project(string name)
+ {
+ return Projects.SingleOrDefault(p => p.Name.Equals(name));
+ }
+ }
+
+ [Subject(typeof(TeamcityStatusParser))]
+ public class When_I_try_to_parse_the_teamcity_status_html : TeamcityStatusParserSpecs
+ {
+ Establish context = () => TestDataFile = "test1";
+ Because I = parsed_the_html;
+ It should_return_a_project = () => the_project("Project A").ShouldNotBeNull();
+ It should_find_all_of_the_builds = () => the_project("Project A").Builds.Length.ShouldEqual(4);
+ It should_find_failed_builds = () => the_project("Website").Builds.First(b => b.Name.Equals("Site")).State.ShouldEqual(BuildState.Failed);
+ }
+}
10,062 Teamcity.Rx.Specs/test_data/test1.html
10,062 additions, 0 deletions not shown
26 Teamcity.Rx.sln
@@ -0,0 +1,26 @@
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Teamcity.Rx", "Teamcity.Rx\Teamcity.Rx.csproj", "{3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Teamcity.Rx.Specs", "Teamcity.Rx.Specs\Teamcity.Rx.Specs.csproj", "{84D63EEB-12A8-4D94-9E63-F91B3859DDA7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {84D63EEB-12A8-4D94-9E63-F91B3859DDA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {84D63EEB-12A8-4D94-9E63-F91B3859DDA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {84D63EEB-12A8-4D94-9E63-F91B3859DDA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {84D63EEB-12A8-4D94-9E63-F91B3859DDA7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
19 Teamcity.Rx/ActionDisposable.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Teamcity.Rx
+{
+ internal class ActionDisposable : IDisposable
+ {
+ private readonly Action _action;
+
+ public ActionDisposable(Action action)
+ {
+ _action = action;
+ }
+
+ void IDisposable.Dispose()
+ {
+ _action();
+ }
+ }
+}
82 Teamcity.Rx/BackgroundWorkerObservable.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Teamcity.Rx
+{
+ internal abstract class BackgroundWorkerObservable<T> : IObservable<T>
+ {
+ private readonly BackgroundWorker _worker;
+ private readonly List<IObserver<T>> _observers = new List<IObserver<T>>();
+ private readonly object _observersLock = new object();
+ private bool _doWork;
+
+ protected BackgroundWorkerObservable()
+ {
+ _worker = new BackgroundWorker();
+ _worker.DoWork += (s, e) => Work();
+ }
+
+ protected abstract T GetObservableEvents();
+
+ protected void Start()
+ {
+ _doWork = true;
+ _worker.RunWorkerAsync();
+ }
+
+ protected void Stop()
+ {
+ _worker.CancelAsync();
+ _doWork = false;
+ }
+
+ private void Work()
+ {
+ while (_doWork)
+ {
+ var events = GetObservableEvents();
+
+ PushToObservers(o => o.OnNext(events));
+ }
+ }
+
+ protected void OnError(Exception ex)
+ {
+ PushToObservers(o => o.OnError(ex));
+ }
+
+ private void PushToObservers(Action<IObserver<T>> action)
+ {
+ lock (_observersLock)
+ {
+ foreach (var observer in _observers)
+ {
+ action(observer);
+ }
+ }
+ }
+
+
+ public IDisposable Subscribe(IObserver<T> observer)
+ {
+ lock(_observersLock)
+ {
+ _observers.Add(observer);
+ }
+
+ return new ActionDisposable(() => Remove(observer));
+ }
+
+ private void Remove(IObserver<T> observer)
+ {
+ lock (_observersLock)
+ {
+ if (_observers.Contains(observer))
+ {
+ _observers.Remove(observer);
+ }
+ }
+ }
+ }
+}
91 Teamcity.Rx/Build.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Runtime.Serialization;
+using Teamcity.Rx.Events;
+
+namespace Teamcity.Rx
+{
+ [DataContract(Name = "Build")]
+ [DebuggerDisplay("{Name} ({Id}) - {State}")]
+ public class Build
+ {
+ [DataMember]
+ public string Id { get; set; }
+
+ [DataMember]
+ public string Name { get; set; }
+
+ [DataMember]
+ public BuildState State { get; set; }
+
+ [DataMember]
+ public string Url { get; set; }
+
+ [DataMember]
+ public string CurrentBuildNumber { get; set; }
+
+ [DataMember]
+ public string CurrentBuildUrl { get; set; }
+
+ public Project Project { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ return obj.GetType() == typeof (Build) && Equals((Build) obj);
+ }
+
+ public bool Equals(Build other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ return ReferenceEquals(this, other) || Equals(other.Id, Id);
+ }
+
+ public override int GetHashCode()
+ {
+ return (Id != null ? Id.GetHashCode() : 0);
+ }
+
+ public static bool operator ==(Build left, Build right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Build left, Build right)
+ {
+ return false == Equals(left, right);
+ }
+
+ public IEnumerable<ITeamcityEvent> Diff(Build b1)
+ {
+ var updatedValues = new List<Expression<Func<Build, object>>>();
+
+ if(State != b1.State)
+ {
+ updatedValues.Add(b => b.State);
+ }
+
+ if(false == Name.Equals(b1.Name))
+ {
+ updatedValues.Add(b => b.Name);
+ }
+
+ if(false == Url.Equals(b1.Url))
+ {
+ updatedValues.Add(b => b.Url);
+ }
+
+ if(updatedValues.Count > 0)
+ {
+ yield return new BuildUpdated(this, updatedValues);
+ }
+ }
+
+ internal Build()
+ {
+ }
+ }
+}
7 Teamcity.Rx/BuildState.cs
@@ -0,0 +1,7 @@
+namespace Teamcity.Rx
+{
+ public enum BuildState
+ {
+ Unkown = 0, Success, Failed, Building, Error
+ }
+}
11 Teamcity.Rx/EmptyDisposable.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Teamcity.Rx
+{
+ internal class EmptyDisposable : IDisposable
+ {
+ public void Dispose()
+ {
+ }
+ }
+}
27 Teamcity.Rx/Events/BuildAdded.cs
@@ -0,0 +1,27 @@
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Events
+{
+ [DataContract(Name = "AddedBuild")]
+ public class BuildAdded : ITeamcityEvent
+ {
+ private readonly Build _build;
+
+ [DataMember]
+ public string EventId
+ {
+ get { return "ab"; }
+ }
+
+ [DataMember]
+ public Build Build
+ {
+ get { return _build; }
+ }
+
+ public BuildAdded(Build build)
+ {
+ _build = build;
+ }
+ }
+}
35 Teamcity.Rx/Events/BuildRemoved.cs
@@ -0,0 +1,35 @@
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Events
+{
+ [DataContract(Name = "BuildRemoved")]
+ public class BuildRemoved : ITeamcityEvent
+ {
+ private readonly string _buildId;
+ private readonly string _projectId;
+
+ [DataMember]
+ public string EventId
+ {
+ get { return "rb"; }
+ }
+
+ [DataMember]
+ public string BuildId
+ {
+ get { return _buildId; }
+ }
+
+ [DataMember]
+ public string ProjectId
+ {
+ get { return _projectId; }
+ }
+
+ public BuildRemoved(Build build)
+ {
+ _buildId = build.Id;
+ _projectId = build.Project.Id;
+ }
+ }
+}
68 Teamcity.Rx/Events/BuildUpdated.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Events
+{
+ [DataContract(Name = "BuildUpdated")]
+ public class BuildUpdated : ITeamcityEvent
+ {
+ private readonly Dictionary<string, object> _updatedValues = new Dictionary<string, object>();
+ private readonly string _buildId;
+
+ [DataMember]
+ public Dictionary<string, object> UpdatedValues
+ {
+ get { return _updatedValues; }
+ }
+
+ [DataMember]
+ public string BuildId
+ {
+ get { return _buildId; }
+ }
+
+ [DataMember]
+ public string EventId
+ {
+ get { return "ub"; }
+ }
+
+ public BuildUpdated(Build entity, IEnumerable<Expression<Func<Build, object>>> updatedValues)
+ {
+ _buildId = entity.Id;
+
+ foreach (var updatedValue in updatedValues)
+ {
+ _updatedValues[GetMemberInfo(updatedValue).Member.Name] = updatedValue.Compile().Invoke(entity);
+ }
+ }
+
+ public override string ToString()
+ {
+ var values = UpdatedValues.Select((prop, value) => string.Format("{0} -> {1}", prop, value)).ToArray();
+
+ return string.Format("Build updated. Updated values\n{0}", string.Join("\t", values));
+ }
+
+ private static MemberExpression GetMemberInfo(LambdaExpression lambda)
+ {
+ if (lambda == null)
+ {
+ throw new ArgumentNullException("lambda", "Expression must be a lambda expression");
+ }
+
+ switch (lambda.Body.NodeType)
+ {
+ case ExpressionType.Convert:
+ return ((UnaryExpression)lambda.Body).Operand as MemberExpression;
+ case ExpressionType.MemberAccess:
+ return lambda.Body as MemberExpression;
+ default:
+ throw new ArgumentException("Expression type must be convert or memberaccess", "lambda");
+ }
+ }
+ }
+}
7 Teamcity.Rx/Events/ITeamcityEvent.cs
@@ -0,0 +1,7 @@
+namespace Teamcity.Rx.Events
+{
+ public interface ITeamcityEvent
+ {
+ string EventId { get; }
+ }
+}
27 Teamcity.Rx/Events/ProjectAdded.cs
@@ -0,0 +1,27 @@
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Events
+{
+ [DataContract(Name = "AddedProject")]
+ public class ProjectAdded : ITeamcityEvent
+ {
+ private readonly Project _project;
+
+ [DataMember]
+ public Project Project
+ {
+ get { return _project; }
+ }
+
+ [DataMember]
+ public string EventId
+ {
+ get { return "ap"; }
+ }
+
+ public ProjectAdded(Project project)
+ {
+ _project = project;
+ }
+ }
+}
22 Teamcity.Rx/Events/ProjectRemoved.cs
@@ -0,0 +1,22 @@
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Events
+{
+ [DataContract(Name = "ProjectRemoved")]
+ public class ProjectRemoved : ITeamcityEvent
+ {
+ [DataMember]
+ public string ProjectId { get; private set; }
+
+ [DataMember]
+ public string EventId
+ {
+ get { return "rp"; }
+ }
+
+ public ProjectRemoved(Project project)
+ {
+ ProjectId = project.Id;
+ }
+ }
+}
53 Teamcity.Rx/Parser/BuildRowParser.cs
@@ -0,0 +1,53 @@
+using System.Text.RegularExpressions;
+using HtmlAgilityPack;
+
+namespace Teamcity.Rx.Parser
+{
+ internal class BuildRowParser : RowParser
+ {
+ public Build Parse(Project project, HtmlNode[] columns)
+ {
+ if (columns.Length != 3)
+ {
+ throw new TeamcityStatusParseException("The build row had the incorrect number of columns");
+ }
+
+ var buildConfigurationColumn = columns[0];
+ var buildConfigurationLink = buildConfigurationColumn.SelectSingleNode("a");
+ var buildConfigurationUrl = buildConfigurationLink.Attributes["href"].Value;
+ var currentBuildLink = columns[1].SelectSingleNode("div/a");
+
+ var build = new Build
+ {
+ Project = project,
+ Name = buildConfigurationLink.InnerText,
+ Id = GetId(buildConfigurationUrl),
+ State = GetBuildState(buildConfigurationColumn),
+ Url = buildConfigurationUrl
+ };
+
+ if (currentBuildLink != null)
+ {
+ build.CurrentBuildNumber = currentBuildLink.InnerText.Replace("#", string.Empty);
+ build.CurrentBuildUrl = currentBuildLink.Attributes["href"].Value;
+ }
+
+ return build;
+ }
+
+ private static BuildState GetBuildState(HtmlNode buildConfigurationColumn)
+ {
+ var result = Regex.Match(buildConfigurationColumn.SelectSingleNode("img").Attributes["src"].Value, @"buildStates/(.*)\.gif");
+
+ switch (result.Groups[1].Value.ToLower())
+ {
+ case "success":
+ return BuildState.Success;
+ case "error":
+ return BuildState.Failed;
+ default:
+ return BuildState.Unkown;
+ }
+ }
+ }
+}
29 Teamcity.Rx/Parser/ProjectRowParser.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using HtmlAgilityPack;
+
+namespace Teamcity.Rx.Parser
+{
+ internal class ProjectRowParser : RowParser
+ {
+ public Project TryParse(HtmlNode[] rowColumns)
+ {
+ var col = rowColumns.First();
+ if (false == col.Attributes["class"].Value.Equals("tcTD_projectName"))
+ {
+ return null;
+ }
+
+ var link = col.SelectSingleNode("div/a");
+ var url = link.Attributes["href"].Value;
+ var id = GetId(url);
+ var name = link.InnerText;
+
+ return new Project
+ {
+ Name = name,
+ Url = url,
+ Id = id
+ };
+ }
+ }
+}
19 Teamcity.Rx/Parser/RowParser.cs
@@ -0,0 +1,19 @@
+using System.Text.RegularExpressions;
+
+namespace Teamcity.Rx.Parser
+{
+ internal abstract class RowParser
+ {
+ protected string GetId(string url)
+ {
+ var match = Regex.Match(url, @"Id=(.*)\&");
+
+ if (false == match.Success || match.Groups.Count != 2)
+ {
+ throw new TeamcityStatusParseException("Could not extract the id from the url {0}", url);
+ }
+
+ return match.Groups[1].Value;
+ }
+ }
+}
37 Teamcity.Rx/Parser/TeamcityStatusParseException.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Teamcity.Rx.Parser
+{
+ [Serializable]
+ public class TeamcityStatusParseException : Exception
+ {
+ //
+ // For guidelines regarding the creation of new exception types, see
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
+ // and
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
+ //
+
+ public TeamcityStatusParseException()
+ {
+ }
+
+ public TeamcityStatusParseException(string message, params object[] args)
+ : base(string.Format(message, args))
+ {
+ }
+
+ public TeamcityStatusParseException(string message, Exception inner)
+ : base(message, inner)
+ {
+ }
+
+ protected TeamcityStatusParseException(
+ SerializationInfo info,
+ StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
68 Teamcity.Rx/Parser/TeamcityStatusParser.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using HtmlAgilityPack;
+using log4net;
+
+namespace Teamcity.Rx.Parser
+{
+ public class TeamcityStatusParser : IObservable<List<Project>>
+ {
+ private static readonly ILog _log = LogManager.GetLogger(typeof(TeamcityStatusParser));
+ private readonly List<Project> _projects;
+
+ public TeamcityStatusParser(string html)
+ {
+ _projects = Parse(html);
+ }
+
+ private static List<Project> Parse(string html)
+ {
+ var doc = new HtmlDocument();
+ doc.LoadHtml(html);
+
+ var projects = new List<Project>();
+ var projectParser = new ProjectRowParser();
+ var buildRowParser = new BuildRowParser();
+
+ Project currentProject = null;
+
+ foreach (var row in doc.DocumentNode.SelectNodes("//tr"))
+ {
+ var columns = row.SelectNodes("td").ToArray();
+
+ if (columns.Count() < 1)
+ {
+ continue;
+ }
+
+ var project = projectParser.TryParse(columns);
+
+ if (null != project)
+ {
+ projects.Add(project);
+ currentProject = project;
+ }
+ else
+ {
+ if (null == currentProject)
+ {
+ throw new TeamcityStatusParseException("Trying to parse a build but there is no project associated with it", new NullReferenceException());
+ }
+
+ currentProject.AddBuild(buildRowParser.Parse(currentProject, columns));
+ }
+ }
+
+ return projects;
+ }
+
+ public IDisposable Subscribe(IObserver<List<Project>> observer)
+ {
+ observer.OnNext(_projects);
+ observer.OnCompleted();
+
+ return new EmptyDisposable();
+ }
+ }
+}
117 Teamcity.Rx/Project.cs
@@ -0,0 +1,117 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.Serialization;
+using Teamcity.Rx.Events;
+
+namespace Teamcity.Rx
+{
+ [DataContract(Name = "Project")]
+ [DebuggerDisplay("{Name} ({Id}) - {Builds.Length} Builds")]
+ public class Project : IEnumerable<Build>
+ {
+ private readonly Dictionary<string, Build> _builds = new Dictionary<string, Build>();
+
+ [DataMember]
+ public string Id { get; set; }
+
+ [DataMember]
+ public string Name { get; set; }
+
+ [DataMember]
+ public string Description { get; set; }
+
+ [DataMember]
+ public string Url { get; set; }
+
+ [DataMember]
+ public Build[] Builds
+ {
+ get { return this.ToArray(); }
+ }
+
+ internal Project()
+ {
+ }
+
+ public void AddBuild(Build build)
+ {
+ _builds[build.Name] = build;
+ }
+
+ public Build this[string buildName]
+ {
+ get
+ {
+ Build build;
+
+ return _builds.TryGetValue(buildName, out build) ? build : null;
+ }
+ }
+
+ public IEnumerator<Build> GetEnumerator()
+ {
+ return _builds.Values.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != typeof (Project)) return false;
+ return Equals((Project) obj);
+ }
+
+ public bool Equals(Project other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Equals(other.Id, Id);
+ }
+
+ public override int GetHashCode()
+ {
+ return (Id != null ? Id.GetHashCode() : 0);
+ }
+
+ public static bool operator ==(Project left, Project right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Project left, Project right)
+ {
+ return !Equals(left, right);
+ }
+
+ public IEnumerable<ITeamcityEvent> Diff(Project p1)
+ {
+ foreach (var build in _builds.Where(build => false == p1._builds.ContainsKey(build.Key)))
+ {
+ yield return new BuildAdded(build.Value);
+ }
+
+ foreach (var build in p1._builds.Where(build => false == _builds.ContainsKey(build.Key)))
+ {
+ yield return new BuildRemoved(build.Value);
+ }
+
+ var buildDifferences = from project in p1.Intersect(this)
+ let build1 = p1.First(s => s.Id.Equals(project.Id))
+ let build2 = this.First(s => s.Id.Equals(project.Id))
+ from difference in build2.Diff(build1)
+ select difference;
+
+ foreach (var difference in buildDifferences)
+ {
+ yield return difference;
+ }
+ }
+ }
+}
37 Teamcity.Rx/Properties/AssemblyInfo.cs
@@ -0,0 +1,37 @@
+using 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("Teamcity.Rx")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Teamcity.Rx")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[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("78b7d865-1c5c-4810-aa61-b24a3164a141")]
+
+// 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")]
+[assembly: InternalsVisibleTo("Teamcity.Rx.Specs")]
91 Teamcity.Rx/Teamcity.Rx.csproj
@@ -0,0 +1,91 @@
+<?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>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{3EC58DDB-9AFE-405E-B8D7-2DC64FE3D9CD}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Teamcity.Rx</RootNamespace>
+ <AssemblyName>Teamcity.Rx</AssemblyName>
+ <TargetFrameworkVersion>v4.0</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="Hammock">
+ <HintPath>..\lib\Hammock.dll</HintPath>
+ </Reference>
+ <Reference Include="HtmlAgilityPack">
+ <HintPath>..\lib\HtmlAgilityPack.dll</HintPath>
+ </Reference>
+ <Reference Include="log4net">
+ <HintPath>..\lib\log4net.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.CoreEx">
+ <HintPath>..\lib\System.CoreEx.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive">
+ <HintPath>..\lib\System.Reactive.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ <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="ActionDisposable.cs" />
+ <Compile Include="BackgroundWorkerObservable.cs" />
+ <Compile Include="Build.cs" />
+ <Compile Include="BuildState.cs" />
+ <Compile Include="EmptyDisposable.cs" />
+ <Compile Include="Events\BuildAdded.cs" />
+ <Compile Include="Events\BuildRemoved.cs" />
+ <Compile Include="Events\BuildUpdated.cs" />
+ <Compile Include="Events\ITeamcityEvent.cs" />
+ <Compile Include="Events\ProjectAdded.cs" />
+ <Compile Include="Events\ProjectRemoved.cs" />
+ <Compile Include="Parser\BuildRowParser.cs" />
+ <Compile Include="Parser\ProjectRowParser.cs" />
+ <Compile Include="Parser\RowParser.cs" />
+ <Compile Include="Parser\TeamcityStatusParseException.cs" />
+ <Compile Include="Parser\TeamcityStatusParser.cs" />
+ <Compile Include="Project.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Teamcity.cs" />
+ <Compile Include="TeamcityEventObservable.cs" />
+ <Compile Include="TeamcityStatusObservable.cs" />
+ <Compile Include="TeamcityObservable.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>
60 Teamcity.Rx/Teamcity.cs
@@ -0,0 +1,60 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Teamcity.Rx
+{
+ [DebuggerDisplay("{_projects.Values.Count} Projects")]
+ public class Teamcity : IEnumerable<Project>
+ {
+ private readonly object _projectsLock = new object();
+ private readonly Dictionary<string, Project> _projects = new Dictionary<string, Project>();
+
+ public bool Loaded { get; private set; }
+
+ public Project this[string projectId]
+ {
+ get
+ {
+ lock (_projectsLock)
+ {
+ Project project;
+
+ return _projects.TryGetValue(projectId, out project) ? project : null;
+ }
+ }
+ }
+
+ internal void UpdateProjects(List<Project> projects)
+ {
+ lock (_projectsLock)
+ {
+ Loaded = true;
+
+ _projects.Clear();
+
+ foreach (var project in projects)
+ {
+ _projects[project.Name] = project;
+ }
+ }
+ }
+
+ internal Teamcity()
+ {
+ }
+
+ public IEnumerator<Project> GetEnumerator()
+ {
+ lock (_projectsLock)
+ {
+ return _projects.Values.GetEnumerator();
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
59 Teamcity.Rx/TeamcityEventObservable.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Teamcity.Rx.Events;
+
+namespace Teamcity.Rx
+{
+ internal class TeamcityEventObservable : IObservable<ITeamcityEvent>
+ {
+ private readonly IEnumerable<ITeamcityEvent> _events;
+
+ public TeamcityEventObservable(IEnumerable<Project> teamcity, IEnumerable<Project> projects)
+ {
+ _events = DiffAllProjects(teamcity.ToList(), projects.ToList());
+ }
+
+ public IEnumerable<ITeamcityEvent> DiffAllProjects(List<Project> oldProjects, List<Project> newProjects)
+ {
+ //Projects added = all projects that are in the new projects but not in the old projects
+ foreach (var project in newProjects.Where(project => false == oldProjects.Exists(o => o.Name == project.Name)))
+ {
+ yield return new ProjectAdded(project);
+ }
+
+ //Projects removed = all projects that were in the old projects but not in the new projects
+ foreach (var project in oldProjects.Where(project => false == newProjects.Exists(o => o.Name == project.Name)))
+ {
+ yield return new ProjectRemoved(project);
+ }
+
+ foreach (var difference in from project in oldProjects.Intersect(newProjects)
+ let oldProject = oldProjects.First(s => s.Id.Equals(project.Id))
+ let newProject = newProjects.First(s => s.Id.Equals(project.Id))
+ from difference in DiffProject(oldProject, newProject)
+ select difference)
+ {
+ yield return difference;
+ }
+ }
+
+ private IEnumerable<ITeamcityEvent> DiffProject(Project oldProject, Project newProject)
+ {
+ yield break;
+ }
+
+
+ public IDisposable Subscribe(IObserver<ITeamcityEvent> observer)
+ {
+ foreach (var @event in _events)
+ {
+ observer.OnNext(@event);
+ }
+
+ observer.OnCompleted();
+
+ return new EmptyDisposable();
+ }
+ }
+}
34 Teamcity.Rx/TeamcityObservable.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Teamcity.Rx.Events;
+using Teamcity.Rx.Parser;
+
+namespace Teamcity.Rx
+{
+ public class TeamcityObservable : IObservable<ITeamcityEvent>
+ {
+ private readonly Teamcity _teamcity;
+ private readonly IObservable<ITeamcityEvent> _events;
+
+ public Teamcity Teamcity
+ {
+ get { return _teamcity; }
+ }
+
+ public TeamcityObservable(string teamcityUrl, string username, string password)
+ {
+ _teamcity = new Teamcity();
+ _events = from status in new TeamcityStatusObservable(teamcityUrl, username, password)
+ from projects in new TeamcityStatusParser(status)
+ from teamcityEvent in new TeamcityEventObservable(_teamcity, projects)
+ select teamcityEvent;
+ }
+
+ public IDisposable Subscribe(IObserver<ITeamcityEvent> observer)
+ {
+ return _events.Subscribe(observer);
+ }
+ }
+}
44 Teamcity.Rx/TeamcityStatusObservable.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Net;
+using System.Threading;
+using Hammock;
+using Hammock.Authentication;
+using Hammock.Authentication.Basic;
+
+namespace Teamcity.Rx
+{
+ internal class TeamcityStatusObservable : BackgroundWorkerObservable<string>
+ {
+ private readonly IWebCredentials _credentials;
+ private readonly IRestClient _client;
+
+ public TeamcityStatusObservable(string teamcityUrl, string username, string password)
+ {
+ _credentials = new BasicAuthCredentials {Username = username, Password = password};
+ _client = new RestClient {Authority = teamcityUrl};
+
+ Start();
+ }
+
+ protected override string GetObservableEvents()
+ {
+ try
+ {
+ var response = _client.Request(new RestRequest { Credentials = _credentials, Path = "externalStatus.html" });
+
+ if (response.StatusCode != HttpStatusCode.OK)
+ {
+ OnError(new Exception(string.Format("{0} returned while trying to get the external status", response.StatusCode)));
+ }
+
+ return response.Content;
+ }
+ catch (Exception ex)
+ {
+ OnError(ex);
+
+ return null;
+ }
+ }
+ }
+}
BIN  lib/Hammock.dll
Binary file not shown
BIN  lib/Hammock.pdb
Binary file not shown
BIN  lib/HtmlAgilityPack.dll
Binary file not shown
BIN  lib/HtmlAgilityPack.pdb
Binary file not shown
BIN  lib/Machine.Specifications.dll
Binary file not shown
BIN  lib/Newtonsoft.Json.dll
Binary file not shown
BIN  lib/System.CoreEx.dll
Binary file not shown
BIN  lib/System.Reactive.dll
Binary file not shown
BIN  lib/log4net.dll
Binary file not shown
Please sign in to comment.
Something went wrong with that request. Please try again.