diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 00000000..67f8ea04 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 00000000..c41a0d0d Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 00000000..3f8c37b2 --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,144 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + + + + $(MSBuildProjectDirectory)\packages.config + $(PackagesProjectConfig) + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 "$(NuGetExePath)" + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 4db5e6a8..764246d5 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,18 @@ -#TeamCitySharp +#TeamCitySharp fork by @borismod -*.NET Library to access TeamCity via their REST API. +* Fork of Paul Slack's original .NET Library to access TeamCity via their REST API. This fork has some additional features, bug fixes and breaking changes. Tested on TeamCity 8.0.5 and 9.1 For more information on TeamCity visit: -http://www.jetbrains.com/teamcity - -##Releases -Please find the release notes [here](https://github.com/stack72/TeamCitySharp/releases) +[TeamCity REST API v9](https://confluence.jetbrains.com/display/TCD9/REST+API) ##License http://stack72.mit-license.org/ ##Installation -There are 2 ways to use TeamCitySharp: +Currently there is one way of using TeamCitySharp: -* install-package TeamCitysharp (via Nuget) * Download source and compile -##Build Monitor -* There is a sample build monitor built with TeamCitySharp. It can be found at [TeamCityMonitor](https://github.com/stack72/TeamCityMonitor) - ##Sample Usage To get a list of projects @@ -58,6 +51,7 @@ There are many tasks that the TeamCity API can do for us. TeamCitySharp groups t * VcsRoots * Changes * BuildArtifacts +* TestOccurrences Each area has its own list of methods available @@ -121,6 +115,12 @@ Each area has its own list of methods available void DeleteAllBuildTypeParameters(BuildTypeLocator locator); void PutAllBuildTypeParameters(BuildTypeLocator locator, IDictionary parameters); void DownloadConfiguration(BuildTypeLocator locator, Action downloadHandler); + + void TriggerBuildConfiguration(string buildConfigId) + void TriggerBuildConfiguration(string buildConfigId, Property[] properties); + void TriggerBuildConfiguration(string buildConfigId, int agentId, Property[] properties) + + void UpdateName(BuildtypeLocator buildTypeLocator, string newName); ###ServerInformation Server ServerInfo(); @@ -153,10 +153,27 @@ Each area has its own list of methods available Change ByChangeId(string id); Change LastChangeDetailByBuildConfigId(string buildConfigId); List ByBuildConfigId(string buildConfigId); + + // Return list of changes with basic information + List ByBuildId(long buildId); + + /// Returns list of changes with their details + List ByBuildIdWithDetails(long buildId); ###BuildArtifacts void DownloadArtifactsByBuildId(string buildId, Action downloadHandler); +###TestOccurrences + List TestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100); + List FailedTestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100); + + /// Retrieves an instance of TestOccurence by Id as received from TeamCity API + TestOccurrence TestOccurrenceById(string testOccurenceLocator); + +###Investigations + List InvestigationsById(string testId); + List InvestigationsByName(string testName); + ##Credits Copyright (c) 2013 Paul Stack (@stack72) @@ -169,4 +186,4 @@ Thanks to the following contributors: * Alexander Fast (@mizipzor) * Serge Baltic * Philipp Dolder -* Mark deVilliers \ No newline at end of file +* Mark deVilliers diff --git a/TeamCitySharp.sln b/TeamCitySharp.sln index f368ed68..edb2ad82 100644 --- a/TeamCitySharp.sln +++ b/TeamCitySharp.sln @@ -1,13 +1,22 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{263F35DD-20C1-4209-B121-E962C9328C70}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamCitySharp.IntegrationTests", "src\Tests\IntegrationTests\TeamCitySharp.IntegrationTests.csproj", "{BA409A09-CC7B-4A71-A3D4-FE27234A721B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamCitySharp", "src\TeamCitySharp\TeamCitySharp.csproj", "{87598714-132F-478E-866E-8C1AF3E83057}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamCitySharp.UnitTests", "src\Tests\UnitTests\TeamCitySharp.UnitTests.csproj", "{1DA175C4-2A6F-4B52-A9B4-87D382150AE9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamCitySharp.Tests", "src\Tests\UnitTests\TeamCitySharp.Tests.csproj", "{1DA175C4-2A6F-4B52-A9B4-87D382150AE9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{21AC60B5-105A-41D2-8A42-2CBA14B6D128}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/packages/EasyHttp.1.6.1.0/EasyHttp.1.6.1.0.nupkg b/packages/EasyHttp.1.6.1.0/EasyHttp.1.6.1.0.nupkg deleted file mode 100644 index 947bc18f..00000000 Binary files a/packages/EasyHttp.1.6.1.0/EasyHttp.1.6.1.0.nupkg and /dev/null differ diff --git a/packages/EasyHttp.1.6.1.0/lib/net40/EasyHttp.dll b/packages/EasyHttp.1.6.1.0/lib/net40/EasyHttp.dll deleted file mode 100644 index 109d6c89..00000000 Binary files a/packages/EasyHttp.1.6.1.0/lib/net40/EasyHttp.dll and /dev/null differ diff --git a/packages/EasyHttp.1.6.1.0/lib/sl40-wp/EasyHttp.dll b/packages/EasyHttp.1.6.1.0/lib/sl40-wp/EasyHttp.dll deleted file mode 100644 index 109d6c89..00000000 Binary files a/packages/EasyHttp.1.6.1.0/lib/sl40-wp/EasyHttp.dll and /dev/null differ diff --git a/packages/EasyHttp.1.6.1.0/lib/sl40/EasyHttp.dll b/packages/EasyHttp.1.6.1.0/lib/sl40/EasyHttp.dll deleted file mode 100644 index 109d6c89..00000000 Binary files a/packages/EasyHttp.1.6.1.0/lib/sl40/EasyHttp.dll and /dev/null differ diff --git a/packages/EasyHttp.1.6.86.0/EasyHttp.1.6.86.0.nupkg b/packages/EasyHttp.1.6.86.0/EasyHttp.1.6.86.0.nupkg new file mode 100644 index 00000000..dadf0dcf Binary files /dev/null and b/packages/EasyHttp.1.6.86.0/EasyHttp.1.6.86.0.nupkg differ diff --git a/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.dll b/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.dll new file mode 100644 index 00000000..8b564246 Binary files /dev/null and b/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.dll differ diff --git a/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.pdb b/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.pdb new file mode 100644 index 00000000..f47e5201 Binary files /dev/null and b/packages/EasyHttp.1.6.86.0/lib/net40/EasyHttp.pdb differ diff --git a/packages/repositories.config b/packages/repositories.config index bcd17bb0..e2cb9c26 100644 --- a/packages/repositories.config +++ b/packages/repositories.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/BuildConfigs.cs b/src/TeamCitySharp/ActionTypes/BuildConfigs.cs index cdcd4a0f..f8f098b4 100644 --- a/src/TeamCitySharp/ActionTypes/BuildConfigs.cs +++ b/src/TeamCitySharp/ActionTypes/BuildConfigs.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Mime; +using System.Text; using System.Xml; using EasyHttp.Http; using TeamCitySharp.Connection; @@ -12,9 +14,9 @@ namespace TeamCitySharp.ActionTypes { internal class BuildConfigs : IBuildConfigs { - private readonly TeamCityCaller _caller; + private readonly ITeamCityCaller _caller; - internal BuildConfigs(TeamCityCaller caller) + internal BuildConfigs(ITeamCityCaller caller) { _caller = caller; } @@ -85,6 +87,16 @@ public BuildConfig CreateConfiguration(string projectName, string configurationN return _caller.PostFormat(configurationName, HttpContentTypes.TextPlain, HttpContentTypes.ApplicationJson, "/app/rest/projects/name:{0}/buildTypes", projectName); } + public BuildConfig CreateConfiguration(ProjectLocator projectLocator, string configurationName) + { + return _caller.PostFormat(configurationName, HttpContentTypes.TextPlain, HttpContentTypes.ApplicationJson, "/app/rest/projects/{0}/buildTypes", projectLocator); + } + + public void AttachToTemplate(BuildTypeLocator buildTypeLocator, string buildTemplateId) + { + _caller.PutFormat(buildTemplateId, HttpContentTypes.TextPlain, "/app/rest/buildTypes/{0}/template", buildTypeLocator); + } + public void SetConfigurationSetting(BuildTypeLocator locator, string settingName, string settingValue) { _caller.PutFormat(settingValue, HttpContentTypes.TextPlain, "/app/rest/buildTypes/{0}/settings/{1}", locator, settingName); @@ -158,6 +170,62 @@ public void DownloadConfiguration(BuildTypeLocator locator, Action downl _caller.GetDownloadFormat(downloadHandler, "/app/rest/buildTypes/{0}", locator); } + public BuildConfig CopyBuildConfiguration(BuildTypeLocator buildTypeLocator, ProjectLocator destinationProjectLocator, string newConfigurationName) + { + var data = string.Format(@"", newConfigurationName, buildTypeLocator); + + return _caller.PostFormat(data, HttpContentTypes.ApplicationXml, HttpContentTypes.ApplicationJson, "/app/rest/projects/{0}/buildTypes", destinationProjectLocator); + } + + public void TriggerBuildConfiguration(string buildConfigId) + { + var data = CreateTriggerBody(buildConfigId, null, new Property[0]); + + _caller.PostFormat(data, HttpContentTypes.ApplicationXml, "/app/rest/buildQueue"); + } + + public void TriggerBuildConfiguration(string buildConfigId, Property[] properties) + { + var triggerBody = CreateTriggerBody(buildConfigId, null, properties); + + _caller.PostFormat(triggerBody, HttpContentTypes.ApplicationXml, "/app/rest/buildQueue"); + } + + public void TriggerBuildConfiguration(string buildConfigId, int agentId, Property[] properties) + { + var bodyBuilder = CreateTriggerBody(buildConfigId, agentId, properties); + + _caller.PostFormat(bodyBuilder, HttpContentTypes.ApplicationXml, "/app/rest/buildQueue"); + } + + private static string CreateTriggerBody(string buildConfigId, int? agentId, Property[] properties) + { + var bodyBuilder = new StringBuilder(); + bodyBuilder.Append(@"").AppendLine() + .AppendFormat(@"", buildConfigId).AppendLine(); + + if (agentId.HasValue) + { + bodyBuilder.AppendFormat(@"", agentId).AppendLine(); + } + + if (properties.Any()) + { + bodyBuilder.Append(@"").AppendLine(); + + foreach (var property in properties) + { + bodyBuilder.AppendFormat(@"", property.Name, property.Value).AppendLine(); + } + + bodyBuilder.Append(@"").AppendLine(); + } + + bodyBuilder.Append("").AppendLine(); + + return bodyBuilder.ToString(); + } + public void PostRawAgentRequirement(BuildTypeLocator locator, string rawXml) { _caller.PostFormat(rawXml, HttpContentTypes.ApplicationXml, "/app/rest/buildTypes/{0}/agent-requirements", locator); @@ -209,5 +277,10 @@ public BuildConfig BuildType(BuildTypeLocator locator) return build; } + + public void UpdateName(BuildTypeLocator buildTypeLocator, string newName) + { + _caller.PutFormat(newName, HttpContentTypes.TextPlain, "/app/rest/buildTypes/{0}/name", buildTypeLocator); + } } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/Builds.cs b/src/TeamCitySharp/ActionTypes/Builds.cs index 4c52f340..20435033 100644 --- a/src/TeamCitySharp/ActionTypes/Builds.cs +++ b/src/TeamCitySharp/ActionTypes/Builds.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Reflection; using TeamCitySharp.Connection; using TeamCitySharp.DomainEntities; using TeamCitySharp.Locators; @@ -9,9 +11,9 @@ namespace TeamCitySharp.ActionTypes { internal class Builds : IBuilds { - private readonly TeamCityCaller _caller; + private readonly ITeamCityCaller _caller; - internal Builds(TeamCityCaller caller) + internal Builds(ITeamCityCaller caller) { _caller = caller; } @@ -26,6 +28,20 @@ public List ByBuildLocator(BuildLocator locator) return new List(); } + public List ByBuildLocator(BuildLocator locator, Action buildProperties) + { + var buildPropertyBuilder = new BuildPropertyBuilder(); + buildProperties.Invoke(buildPropertyBuilder); + var buildPropertiesList = buildPropertyBuilder.GetBuildPropertiesList(); + + var buildWrapper = _caller.GetFormat("/app/rest/builds?locator={0}&fields=count,build({1})", locator.ToString(), buildPropertiesList); + if (int.Parse(buildWrapper.Count) > 0) + { + return buildWrapper.Build; + } + return new List(); + } + public Build LastBuildByAgent(string agentName) { return ByBuildLocator(BuildLocator.WithDimensions( @@ -39,6 +55,21 @@ public void Add2QueueBuildByBuildConfigId(string buildConfigId) _caller.GetFormat("/action.html?add2Queue={0}", buildConfigId); } + public Build BuildById(long buildId) + { + Build build = _caller.GetFormat("/app/rest/builds/id:{0}", buildId); + if (build.LastChanges == null) + { + if (build.Changes.Count == 0) + { + build.LastChanges = new ChangesList(); + return build; + } + build.LastChanges = _caller.GetByFullUrl(build.Changes.Href); + } + return build; + } + public List SuccessfulBuildsByBuildConfigId(string buildConfigId) { return ByBuildLocator(BuildLocator.WithDimensions(BuildTypeLocator.WithId(buildConfigId), @@ -146,4 +177,39 @@ public List NonSuccessfulBuildsForUser(string userName) return builds.Where(b => b.Status != "SUCCESS").ToList(); } } + + public class BuildPropertyBuilder + { + readonly IList m_Properties = new List(new[] + { + "buildTypeId", "href", "id", "number", "state", "status","webUrl" + }); + + internal string GetBuildPropertiesList() + { + return string.Join(",", m_Properties); + } + + public BuildPropertyBuilder IncludeStartDate() + { + return IncludeProperty(); + } + + public BuildPropertyBuilder IncludeFinishDate() + { + return IncludeProperty(); + } + + public BuildPropertyBuilder IncludeStatusText() + { + return IncludeProperty(); + } + + private BuildPropertyBuilder IncludeProperty() + { + var methodName = new StackFrame(1).GetMethod().Name.Remove(0,7); + m_Properties.Add(methodName.FirstCharacterToLower()); + return this; + } + } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/Changes.cs b/src/TeamCitySharp/ActionTypes/Changes.cs index 5218b4aa..b518f8d8 100644 --- a/src/TeamCitySharp/ActionTypes/Changes.cs +++ b/src/TeamCitySharp/ActionTypes/Changes.cs @@ -16,21 +16,35 @@ internal Changes(TeamCityCaller caller) public List All() { - var changeWrapper = _caller.Get("/app/rest/changes"); + var changeWrapper = _caller.Get("/app/rest/changes"); return changeWrapper.Change; } - public Change ByChangeId(string id) + public Change ByChangeId(long id) { var change = _caller.GetFormat("/app/rest/changes/id:{0}", id); return change; } + public List ByBuildId(long buildId) + { + var changeWrapper = _caller.GetFormat("/app/rest/changes?locator=build:(id:{0})", buildId); + + return changeWrapper.Change; + } + + public List ByBuildIdWithDetails(long buildId) + { + var changes = ByBuildId(buildId); + + return changes == null ? new List() : changes.Select(c => ByChangeId(c.Id)).ToList(); + } + public List ByBuildConfigId(string buildConfigId) { - var changeWrapper = _caller.GetFormat("/app/rest/changes?buildType={0}", buildConfigId); + var changeWrapper = _caller.GetFormat("/app/rest/changes?buildType={0}", buildConfigId); return changeWrapper.Change; } diff --git a/src/TeamCitySharp/ActionTypes/IBuildConfigs.cs b/src/TeamCitySharp/ActionTypes/IBuildConfigs.cs index 2829ec2d..9df92660 100644 --- a/src/TeamCitySharp/ActionTypes/IBuildConfigs.cs +++ b/src/TeamCitySharp/ActionTypes/IBuildConfigs.cs @@ -18,6 +18,7 @@ public interface IBuildConfigs List ByProjectId(string projectId); List ByProjectName(string projectName); BuildConfig CreateConfiguration(string projectName, string configurationName); + BuildConfig CreateConfiguration(ProjectLocator projectLocator, string configurationName); void SetConfigurationSetting(BuildTypeLocator locator, string settingName, string settingValue); bool GetConfigurationPauseStatus(BuildTypeLocator locator); @@ -88,5 +89,43 @@ public interface IBuildConfigs void PutAllBuildTypeParameters(BuildTypeLocator locator, IDictionary parameters); void DownloadConfiguration(BuildTypeLocator locator, Action downloadHandler); + + /// + /// Copies given build configuration to another project + /// + /// + /// + /// + BuildConfig CopyBuildConfiguration(BuildTypeLocator buildTypeLocator, ProjectLocator destinationProjectLocator, string newConfigurationName); + + /// + /// Triggers build configuration by ID + /// + /// + void TriggerBuildConfiguration(string buildConfigId); + + void AttachToTemplate(BuildTypeLocator buildTypeLocator, string buildTemplateId); + + /// + /// Triggers build configuration by ID and sends parameters to the build + /// + /// + /// + void TriggerBuildConfiguration(string buildConfigId, Property[] properties); + + /// + /// Triggers build configuration by ID on specific agent and sends parameters to the build + /// + /// + /// + /// + void TriggerBuildConfiguration(string buildConfigId, int agentId, Property[] properties); + + /// + /// Update name of build configuration + /// + /// Build Type locator + /// New build configuration name + void UpdateName(BuildTypeLocator buildTypeLocator, string newName); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/IBuilds.cs b/src/TeamCitySharp/ActionTypes/IBuilds.cs index 6afba988..69677878 100644 --- a/src/TeamCitySharp/ActionTypes/IBuilds.cs +++ b/src/TeamCitySharp/ActionTypes/IBuilds.cs @@ -24,5 +24,7 @@ public interface IBuilds List ByBranch(string branchName); Build LastBuildByAgent(string agentName); void Add2QueueBuildByBuildConfigId(string buildConfigId); + Build BuildById(long buildId); + List ByBuildLocator(BuildLocator locator, Action buildProperties); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/IChanges.cs b/src/TeamCitySharp/ActionTypes/IChanges.cs index 1ae51cd5..c39492b7 100644 --- a/src/TeamCitySharp/ActionTypes/IChanges.cs +++ b/src/TeamCitySharp/ActionTypes/IChanges.cs @@ -6,8 +6,10 @@ namespace TeamCitySharp.ActionTypes public interface IChanges { List All(); - Change ByChangeId(string id); + Change ByChangeId(long id); Change LastChangeDetailByBuildConfigId(string buildConfigId); List ByBuildConfigId(string buildConfigId); + List ByBuildId(long buildId); + List ByBuildIdWithDetails(long buildId); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/ITestOccurrences.cs b/src/TeamCitySharp/ActionTypes/ITestOccurrences.cs new file mode 100644 index 00000000..4fd9f004 --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/ITestOccurrences.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using TeamCitySharp.Connection; +using TeamCitySharp.DomainEntities; +using TeamCitySharp.Locators; + +namespace TeamCitySharp.ActionTypes +{ + public interface ITestOccurrences + { + List TestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100); + List FailedTestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100); + + /// + /// Retrieves an instance of TestOccurence by Id as received from TeamCity API + /// + /// In the format id=build:(id:181203),id:305 + /// + TestOccurrence TestOccurrenceById(string testOccurenceLocator); + + /// + /// Retrieve test history by id property on the test element from InvestigationsById API + /// + /// In the format 8191545283982494536 + /// + List TestHistoryByTestId(string testId); + } + + public class TestOccurrences : ITestOccurrences + { + private readonly TeamCityCaller _caller; + + internal TestOccurrences(TeamCityCaller caller) + { + _caller = caller; + } + + public List TestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100) + { + var testOccurrenceWrapper = _caller.GetFormat("/app/rest/testOccurrences?locator=build:{0}", + CreateBuildLocator(buildId, indexStart, maxResults)); + + if (int.Parse(testOccurrenceWrapper.Count) > 0) + { + return testOccurrenceWrapper.TestOccurrence; + } + + return new List(); + } + + public List FailedTestOccurrencesByBuildId(long buildId, int? indexStart = 0, int? maxResults = 100) + { + var testOccurrenceWrapper = _caller.GetFormat("/app/rest/testOccurrences?locator=build:{0},status:FAILURE", + CreateBuildLocator(buildId, indexStart, maxResults)); + + if (int.Parse(testOccurrenceWrapper.Count) > 0) + { + return testOccurrenceWrapper.TestOccurrence; + } + + return new List(); + } + + /// + /// Retrieves an instance of TestOccurence by Id as received from TeamCity API + /// + /// In the format id=build:(id:181203),id:305 + /// + public TestOccurrence TestOccurrenceById(string testOccurenceLocator) + { + var testOccurrence = _caller.GetFormat("/app/rest/testOccurrences/{0}", testOccurenceLocator); + + return testOccurrence; + } + + /// + /// Retrieve test history by id property on the test element from InvestigationsById API + /// + /// In the format 8191545283982494536 + /// + public List TestHistoryByTestId(string testId) + { + var testOccurrence = _caller.GetFormat("/app/rest/testOccurrences?locator=test:id:{0}", testId); + + return testOccurrence.TestOccurrence; + } + + private static BuildLocator CreateBuildLocator(long buildId, int? indexStart, int? maxResults) + { + return BuildLocator.WithDimensions(buildId:buildId, startIndex:indexStart, maxResults: maxResults); + } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/Investigations.cs b/src/TeamCitySharp/ActionTypes/Investigations.cs new file mode 100644 index 00000000..d5e79abb --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/Investigations.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using TeamCitySharp.Connection; +using TeamCitySharp.DomainEntities; +using TeamCitySharp.Locators; + +namespace TeamCitySharp.ActionTypes +{ + public interface IInvestigations + { + /// + /// Returns investigation details about the test by its full name. In the format: + /// MSTest: TestingNamespace.TestingClass.TestMethodName + /// + /// TestLocator to indicate the test to get investigations for + /// + Investigation InvestigationByTest(TestLocator testLocator); + + /// + /// + /// + /// + /// + IList InvestinationsByUser(UserLocator userLocator); + + /// + /// Returns investigation details for build configuration + /// + /// + /// + IList InvestigationsByBuildConfiguration(BuildTypeLocator buildTypeLocator); + } + + public class Investigations : IInvestigations + { + private readonly ITeamCityCaller _caller; + + internal Investigations(ITeamCityCaller caller) + { + _caller = caller; + } + + public Investigation InvestigationByTest(TestLocator testLocator) + { + var investigationWrapper = _caller.GetFormat("/app/rest/investigations?locator=test:({0})", testLocator); + + if (investigationWrapper.Investigation == null || investigationWrapper.Investigation.Count == 0) + { + return null; + } + return investigationWrapper.Investigation[0]; + } + + public IList InvestinationsByUser(UserLocator userLocator) + { + var investigationWrapper = _caller.GetFormat("/app/rest/investigations?locator=assignee:({0})", + userLocator); + + return investigationWrapper.Investigation ?? new List(); + } + + public IList InvestigationsByBuildConfiguration(BuildTypeLocator buildTypeLocator) + { + var investigationWrapper = _caller.GetFormat("/app/rest/investigations?locator=buildType:({0})", + buildTypeLocator); + + return investigationWrapper.Investigation ?? new List(); + } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/ProjectLocator.cs b/src/TeamCitySharp/ActionTypes/ProjectLocator.cs new file mode 100644 index 00000000..37c41c34 --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/ProjectLocator.cs @@ -0,0 +1,27 @@ +namespace TeamCitySharp.ActionTypes +{ + public class ProjectLocator + { + public string Id { get; private set; } + public string Name { get; private set; } + + public static ProjectLocator WithId(string id) + { + return new ProjectLocator {Id = id}; + } + + public static ProjectLocator WithName(string name) + { + return new ProjectLocator {Name = name}; + } + + public override string ToString() + { + if (!string.IsNullOrEmpty(Id)) + { + return "id:" + Id; + } + return "name:" + Name; + } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/StringExtensions.cs b/src/TeamCitySharp/ActionTypes/StringExtensions.cs new file mode 100644 index 00000000..b49d80ce --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/StringExtensions.cs @@ -0,0 +1,15 @@ +using System; + +namespace TeamCitySharp.ActionTypes +{ + public static class StringExtensions + { + public static string FirstCharacterToLower(this string str) + { + if (String.IsNullOrEmpty(str) || Char.IsLower(str, 0)) + return str; + + return Char.ToLowerInvariant(str[0]) + str.Substring(1); + } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/Connection/ITeamCityCaller.cs b/src/TeamCitySharp/Connection/ITeamCityCaller.cs index aee4df5c..d35ba4d3 100644 --- a/src/TeamCitySharp/Connection/ITeamCityCaller.cs +++ b/src/TeamCitySharp/Connection/ITeamCityCaller.cs @@ -38,5 +38,6 @@ internal interface ITeamCityCaller void Delete(string urlPart); string GetRaw(string urlPart); + T GetByFullUrl(string fullUrl); } } \ No newline at end of file diff --git a/src/TeamCitySharp/Connection/TeamCityCaller.cs b/src/TeamCitySharp/Connection/TeamCityCaller.cs index 8f2cbadf..5626c8f4 100644 --- a/src/TeamCitySharp/Connection/TeamCityCaller.cs +++ b/src/TeamCitySharp/Connection/TeamCityCaller.cs @@ -67,12 +67,16 @@ public void GetDownloadFormat(Action downloadHandler, string urlPart, pa throw new ArgumentException("If you are not acting as a guest you must supply userName and password"); } - urlPart = System.Web.HttpUtility.UrlEncode(urlPart); if (string.IsNullOrEmpty(urlPart)) { throw new ArgumentException("Url must be specfied"); } + if (urlPart.Contains("+")) + { + urlPart = System.Web.HttpUtility.UrlEncode(urlPart); + } + if (downloadHandler == null) { throw new ArgumentException("A download handler must be specfied."); @@ -83,9 +87,12 @@ public void GetDownloadFormat(Action downloadHandler, string urlPart, pa try { - CreateHttpClient(_configuration.UserName, _configuration.Password, HttpContentTypes.ApplicationJson).GetAsFile(url, tempFileName); + var httpResponse = CreateHttpClient(_configuration.UserName, _configuration.Password, HttpContentTypes.ApplicationJson).GetAsFile(url, tempFileName); + if (httpResponse.StatusCode != HttpStatusCode.OK) + { + throw new HttpException(httpResponse.StatusCode, httpResponse.StatusDescription); + } downloadHandler.Invoke(tempFileName); - } finally { @@ -118,6 +125,13 @@ public string StartBackup(string urlPart) return string.Empty; } + public T GetByFullUrl(string fullUrl) + { + var url = string.Format("{0}{1}{2}", GetProtocol(), _configuration.HostName, fullUrl); + var response = GetResponseByFullUrl(url); + return response.StaticBody(); + } + public T Get(string urlPart) { var response = GetResponse(urlPart); @@ -139,7 +153,13 @@ private HttpResponse GetResponse(string urlPart) var url = CreateUrl(urlPart); - var response = CreateHttpClient(_configuration.UserName, _configuration.Password, HttpContentTypes.ApplicationJson).Get(url); + return GetResponseByFullUrl(url); + } + + private HttpResponse GetResponseByFullUrl(string url) + { + var response = + CreateHttpClient(_configuration.UserName, _configuration.Password, HttpContentTypes.ApplicationJson).Get(url); ThrowIfHttpError(response, url); return response; } @@ -238,15 +258,23 @@ private static void ThrowIfHttpError(HttpResponse response, string url) private string CreateUrl(string urlPart) { - var protocol = _configuration.UseSSL ? "https://" : "http://"; + var protocol = GetProtocol(); var authType = _configuration.ActAsGuest ? "/guestAuth" : "/httpAuth"; return string.Format("{0}{1}{2}{3}", protocol, _configuration.HostName, authType, urlPart); } + private string GetProtocol() + { + return _configuration.UseSSL ? "https://" : "http://"; + } + private HttpClient CreateHttpClient(string userName, string password, string accept) { - var httpClient = new HttpClient(new TeamcityJsonEncoderDecoderConfiguration()); + var httpClient = new HttpClient(new TeamcityJsonEncoderDecoderConfiguration()) + { + ShouldRemoveAtSign = false + }; httpClient.Request.Accept = accept; if (!_configuration.ActAsGuest) { diff --git a/src/TeamCitySharp/DomainEntities/Assignment.cs b/src/TeamCitySharp/DomainEntities/Assignment.cs new file mode 100644 index 00000000..41a27b1e --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/Assignment.cs @@ -0,0 +1,10 @@ +using System; + +namespace TeamCitySharp.DomainEntities +{ + public class Assignment + { + public User User { get; set; } + public DateTime Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/Build.cs b/src/TeamCitySharp/DomainEntities/Build.cs index 7d16fc34..bebd176c 100644 --- a/src/TeamCitySharp/DomainEntities/Build.cs +++ b/src/TeamCitySharp/DomainEntities/Build.cs @@ -1,27 +1,27 @@ using System; -namespace TeamCitySharp.DomainEntities -{ - public class Build - { - public string Id { get; set; } - public string Number { get; set; } - public string Status { get; set; } - public string BuildTypeId { get; set; } - public string Href { get; set; } - public string WebUrl { get; set; } - public string StatusText { get; set; } +namespace TeamCitySharp.DomainEntities +{ + public class Build + { + public long Id { get; set; } + public string Number { get; set; } + public string Status { get; set; } + public string BuildTypeId { get; set; } + public string Href { get; set; } + public string WebUrl { get; set; } + public string StatusText { get; set; } public DateTime StartDate { get; set; } - public DateTime FinishDate { get; set; } - - public BuildConfig BuildConfig { get; set; } - public Agent Agent { get; set;} - public ChangeWrapper Changes { get; set; } - - public override string ToString() - { - return Number; - } - - } + public DateTime FinishDate { get; set; } + + public BuildConfig BuildConfig { get; set; } + public Agent Agent { get; set; } + public ChangesList LastChanges { get; set; } + public ChangesWrapper Changes { get; set; } + + public override string ToString() + { + return Number; + } + } } \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/Change.cs b/src/TeamCitySharp/DomainEntities/Change.cs index d414cf42..7cf91e04 100644 --- a/src/TeamCitySharp/DomainEntities/Change.cs +++ b/src/TeamCitySharp/DomainEntities/Change.cs @@ -1,17 +1,19 @@ using System; -namespace TeamCitySharp.DomainEntities -{ - public class Change - { - public string Username { get; set; } - public string WebLink { get; set; } - public string Href { get; set; } - public string Id { get; set; } +namespace TeamCitySharp.DomainEntities +{ + public class Change + { + public string Username { get; set; } + public string WebLink { get; set; } + public string Href { get; set; } + public long Id { get; set; } public string Version { get; set; } - public DateTime Date { get; set; } - public string Comment { get; set; } - - public FileWrapper Files { get; set; } - } + public DateTime Date { get; set; } + public string Comment { get; set; } + + public FileWrapper Files { get; set; } + + public User User { get; set; } + } } \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/ChangeWrapper.cs b/src/TeamCitySharp/DomainEntities/ChangesList.cs similarity index 81% rename from src/TeamCitySharp/DomainEntities/ChangeWrapper.cs rename to src/TeamCitySharp/DomainEntities/ChangesList.cs index 119d5c47..b9ba774c 100644 --- a/src/TeamCitySharp/DomainEntities/ChangeWrapper.cs +++ b/src/TeamCitySharp/DomainEntities/ChangesList.cs @@ -2,7 +2,7 @@ namespace TeamCitySharp.DomainEntities { - public class ChangeWrapper + public class ChangesList { public List Change { get; set; } } diff --git a/src/TeamCitySharp/DomainEntities/ChangesWrapper.cs b/src/TeamCitySharp/DomainEntities/ChangesWrapper.cs new file mode 100644 index 00000000..ea2c2587 --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/ChangesWrapper.cs @@ -0,0 +1,8 @@ +namespace TeamCitySharp.DomainEntities +{ + public class ChangesWrapper + { + public int Count { get; set; } + public string Href { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/Investigation.cs b/src/TeamCitySharp/DomainEntities/Investigation.cs new file mode 100644 index 00000000..a76f4ec6 --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/Investigation.cs @@ -0,0 +1,10 @@ +namespace TeamCitySharp.DomainEntities +{ + public class Investigation + { + public User Assignee { get; set; } + public Assignment Assignment { get; set; } + public TestOccurrenceWrapper TargetWrap { get; set; } + public ResolutionWrapper Resolution { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/InvestigationWrapper.cs b/src/TeamCitySharp/DomainEntities/InvestigationWrapper.cs new file mode 100644 index 00000000..47ce0100 --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/InvestigationWrapper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using TeamCitySharp.ActionTypes; + +namespace TeamCitySharp.DomainEntities +{ + public class InvestigationWrapper + { + public string Count { get; set; } + public List Investigation { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/ResolutionWrapper.cs b/src/TeamCitySharp/DomainEntities/ResolutionWrapper.cs new file mode 100644 index 00000000..443f86fd --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/ResolutionWrapper.cs @@ -0,0 +1,7 @@ +namespace TeamCitySharp.DomainEntities +{ + public class ResolutionWrapper + { + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/TestOccurrence.cs b/src/TeamCitySharp/DomainEntities/TestOccurrence.cs new file mode 100644 index 00000000..a6c8e1c1 --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/TestOccurrence.cs @@ -0,0 +1,31 @@ +using System.Text.RegularExpressions; + +namespace TeamCitySharp.DomainEntities +{ + public class TestOccurrence + { + public int Duration { get; set; } + public string Href { get; set; } + public string Id { get; set; } + public string Name { get; set; } + public string Status { get; set; } + public string Details { get; set; } + public Test Test { get; set; } + } + + public class Test + { + private static readonly Regex IdRegex = new Regex(@"^\/httpAuth\/app\/rest/tests\/id\:(?\-?\d+)$", RegexOptions.Compiled); + + public string Id + { + get + { + return IdRegex.Match(Href).Groups["id"].Value; + } + } + + public string Name { get; set; } + public string Href { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/DomainEntities/TestOccurrenceWrapper.cs b/src/TeamCitySharp/DomainEntities/TestOccurrenceWrapper.cs new file mode 100644 index 00000000..a9f553a1 --- /dev/null +++ b/src/TeamCitySharp/DomainEntities/TestOccurrenceWrapper.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace TeamCitySharp.DomainEntities +{ + public class TestOccurrenceWrapper + { + public string Count { get; set; } + public List TestOccurrence { get; set; } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/ITeamCityClient.cs b/src/TeamCitySharp/ITeamCityClient.cs index f63f97d3..6b4dd101 100644 --- a/src/TeamCitySharp/ITeamCityClient.cs +++ b/src/TeamCitySharp/ITeamCityClient.cs @@ -1,4 +1,4 @@ -using TeamCitySharp.ActionTypes; +using TeamCitySharp.ActionTypes; namespace TeamCitySharp { @@ -7,7 +7,7 @@ public interface ITeamCityClient void Connect(string userName, string password); void ConnectAsGuest(); bool Authenticate(); - + IBuilds Builds { get; } IBuildConfigs BuildConfigs { get; } IProjects Projects { get; } @@ -16,6 +16,8 @@ public interface ITeamCityClient IAgents Agents { get; } IVcsRoots VcsRoots { get; } IChanges Changes { get; } - IBuildArtifacts Artifacts { get; } + IBuildArtifacts Artifacts { get; } + ITestOccurrences TestOccurrences { get; } + IInvestigations Investigations { get; } } } \ No newline at end of file diff --git a/src/TeamCitySharp/Locators/BuildLocator.cs b/src/TeamCitySharp/Locators/BuildLocator.cs index 1823d6d7..701d3560 100644 --- a/src/TeamCitySharp/Locators/BuildLocator.cs +++ b/src/TeamCitySharp/Locators/BuildLocator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace TeamCitySharp.Locators @@ -40,11 +40,14 @@ public static BuildLocator WithDimensions(BuildTypeLocator buildType = null, BuildLocator sinceBuild = null, DateTime? sinceDate = null, string[] tags = null, - string branch = null + string branch = null, + string buildNumber = null, + long? buildId = null ) { return new BuildLocator { + Id = buildId, BuildType = buildType, User = user, AgentName = agentName, @@ -58,7 +61,8 @@ public static BuildLocator WithDimensions(BuildTypeLocator buildType = null, SinceBuild = sinceBuild, SinceDate = sinceDate, Tags = tags, - Branch = branch + Branch = branch, + Number = buildNumber }; } @@ -81,18 +85,18 @@ public static BuildLocator WithDimensions(BuildTypeLocator buildType = null, public override string ToString() { + var locatorFields = new List(); + if (Id != null) { - return "id:" + Id; + locatorFields.Add("id:" + Id); } if (Number != null) { - return "number:" + Number; + locatorFields.Add("number:" + Number); } - var locatorFields = new List(); - if (BuildType != null) { locatorFields.Add("buildType:(" + BuildType + ")"); diff --git a/src/TeamCitySharp/Locators/TestLocator.cs b/src/TeamCitySharp/Locators/TestLocator.cs new file mode 100644 index 00000000..83966cb4 --- /dev/null +++ b/src/TeamCitySharp/Locators/TestLocator.cs @@ -0,0 +1,34 @@ +using System; + +namespace TeamCitySharp.Locators +{ + public class TestLocator + { + public static TestLocator WithId(string id) + { + return new TestLocator { Id = id }; + } + + public static TestLocator WithName(string number) + { + return new TestLocator { Name = number }; + } + + public string Id { get; private set; } + public string Name { get; private set; } + + public override string ToString() + { + if (!string.IsNullOrEmpty(Id)) + { + return string.Format("id:{0}", Id); + } + if (!string.IsNullOrEmpty(Name)) + { + return string.Format("name:{0}", Name); + } + + throw new Exception("TestLocator properties are not initialized"); + } + } +} \ No newline at end of file diff --git a/src/TeamCitySharp/Properties/AssemblyInfo.cs b/src/TeamCitySharp/Properties/AssemblyInfo.cs index 2b6db0d5..4467edc5 100644 --- a/src/TeamCitySharp/Properties/AssemblyInfo.cs +++ b/src/TeamCitySharp/Properties/AssemblyInfo.cs @@ -35,5 +35,5 @@ [assembly: AssemblyVersion("0.3.3")] [assembly: AssemblyFileVersion("0.3.3")] -[assembly: InternalsVisibleTo("TeamCitySharp.UnitTests")] +[assembly: InternalsVisibleTo("TeamCitySharp.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/TeamCitySharp/TeamCityClient.cs b/src/TeamCitySharp/TeamCityClient.cs index eeb9bb82..ae159741 100644 --- a/src/TeamCitySharp/TeamCityClient.cs +++ b/src/TeamCitySharp/TeamCityClient.cs @@ -1,4 +1,4 @@ -using TeamCitySharp.ActionTypes; +using TeamCitySharp.ActionTypes; using TeamCitySharp.Connection; namespace TeamCitySharp @@ -15,6 +15,8 @@ public class TeamCityClient : IClientConnection, ITeamCityClient private IVcsRoots _vcsRoots; private IChanges _changes; private IBuildArtifacts _artifacts; + private ITestOccurrences _testOccurrences; + private IInvestigations _investigations; public TeamCityClient(string hostName, bool useSsl = false) { @@ -37,8 +39,8 @@ public bool Authenticate() } public IBuilds Builds - { - get { return _builds ?? (_builds = new Builds(_caller)); } + { + get { return _builds ?? (_builds = new Builds(_caller)); } } public IBuildConfigs BuildConfigs @@ -55,8 +57,8 @@ public IServerInformation ServerInformation { get { return _serverInformation ?? (_serverInformation = new ServerInformation(_caller)); } } - - public IUsers Users + + public IUsers Users { get { return _users ?? (_users = new Users(_caller)); } } @@ -72,13 +74,23 @@ public IVcsRoots VcsRoots } public IChanges Changes - { - get { return _changes ?? (_changes = new Changes(_caller)); } + { + get { return _changes ?? (_changes = new Changes(_caller)); } } public IBuildArtifacts Artifacts { get { return _artifacts ?? (_artifacts = new BuildArtifacts(_caller)); } } + + public ITestOccurrences TestOccurrences + { + get { return _testOccurrences ?? (_testOccurrences = new TestOccurrences(_caller)); } + } + + public IInvestigations Investigations + { + get { return _investigations ?? new Investigations(_caller); } + } } -} +} diff --git a/src/TeamCitySharp/TeamCitySharp.csproj b/src/TeamCitySharp/TeamCitySharp.csproj index 2e7df5aa..2be0a489 100644 --- a/src/TeamCitySharp/TeamCitySharp.csproj +++ b/src/TeamCitySharp/TeamCitySharp.csproj @@ -14,6 +14,8 @@ 512 + ..\..\ + true true @@ -33,15 +35,16 @@ 4 - - False - ..\..\packages\EasyHttp.1.6.1.0\lib\net40\EasyHttp.dll + + ..\..\packages\EasyHttp.1.6.86.0\lib\net40\EasyHttp.dll + True ..\..\packages\JsonFx.2.0.1209.2802\lib\net40\JsonFx.dll + @@ -60,13 +63,25 @@ + + + + + + + + + + + + @@ -99,7 +114,7 @@ - + @@ -125,6 +140,13 @@ + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +