Skip to content

Commit 55ad070

Browse files
authored
Fix VS403474 (#139)
* Limit request to 200 work items a time, fixes #138 * Setup for Integration tests using Terraform Squashed commit of the following: commit cd8f95188498387860cc5a2fb18755db8b351e86 Author: Giulio Vian <[email protected]> Date: Thu Jul 9 18:07:10 2020 +0100 fix: missed the RG for Scenario4 commit 74d16de Author: Giulio Vian <[email protected]> Date: Wed Jul 8 23:20:26 2020 +0100 fully scripted integration test environment commit c7e6be3 Author: Giulio Vian <[email protected]> Date: Fri Jul 3 22:51:11 2020 +0100 initial scripts to setup integration test
1 parent eac04df commit 55ad070

File tree

11 files changed

+301
-18
lines changed

11 files changed

+301
-18
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,9 @@ src/aggregator-function/test*/*
344344
.DS_Store
345345
build.sh
346346
*.vsix
347+
outputs
348+
## Terraform stuff
349+
*.tfvars
350+
.terraform
351+
*.tfstate*
352+
src/integrationtests-setup/_plan

src/aggregator-cli.sln

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unittests-core", "unittests
2525
EndProject
2626
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unittests-function", "unittests-function\unittests-function.csproj", "{A875638A-8E95-47BE-AEDD-BAD5113692B3}"
2727
EndProject
28+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "setup-integration-tests", "setup-integration-tests", "{410CBC1F-2AFE-4DDA-9EAC-66FC67DC2D68}"
29+
ProjectSection(SolutionItems) = preProject
30+
integrationtests-setup\logon-data.tmpl = integrationtests-setup\logon-data.tmpl
31+
integrationtests-setup\main.tf = integrationtests-setup\main.tf
32+
integrationtests-setup\output.tf = integrationtests-setup\output.tf
33+
integrationtests-setup\terraform.tf = integrationtests-setup\terraform.tf
34+
integrationtests-setup\variables.tf = integrationtests-setup\variables.tf
35+
EndProjectSection
36+
EndProject
2837
Global
2938
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3039
Debug|Any CPU = Debug|Any CPU

src/aggregator-ruleng/Extensions.cs

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
25
using Microsoft.VisualStudio.Services.Common;
36

47

@@ -10,5 +13,29 @@ public static string GetTeamProject(this WorkItem workItem)
1013
{
1114
return workItem.Fields.GetCastedValueOrDefault("System.TeamProject", default(string));
1215
}
16+
17+
// source https://stackoverflow.com/a/22222439/100864
18+
public static IEnumerable<IEnumerable<T>> Paginate<T>(this IEnumerable<T> source, int pageSize)
19+
{
20+
if (source == null) throw new ArgumentNullException(nameof(source));
21+
if (pageSize <= 1) throw new ArgumentException("Must be greater than 1", nameof(pageSize));
22+
23+
using (var enumerator = source.GetEnumerator())
24+
{
25+
while (enumerator.MoveNext())
26+
{
27+
var currentPage = new List<T>(pageSize)
28+
{
29+
enumerator.Current
30+
};
31+
32+
while (currentPage.Count < pageSize && enumerator.MoveNext())
33+
{
34+
currentPage.Add(enumerator.Current);
35+
}
36+
yield return new ReadOnlyCollection<T>(currentPage);
37+
}
38+
}
39+
}
1340
}
14-
}
41+
}

src/aggregator-ruleng/WorkItemStore.cs

+19-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace aggregator.Engine
1616
{
1717
public class WorkItemStore
1818
{
19+
private const int VS403474_LIMIT = 200;
20+
1921
private readonly EngineContext _context;
2022
private readonly IClientsContext _clients;
2123
private readonly Lazy<Task<IEnumerable<WorkItemTypeCategory>>> _lazyGetWorkItemCategories;
@@ -59,13 +61,23 @@ public WorkItemWrapper GetWorkItem(WorkItemRelationWrapper item)
5961

6062
public IList<WorkItemWrapper> GetWorkItems(IEnumerable<int> ids)
6163
{
62-
_context.Logger.WriteVerbose($"Getting workitems {ids.ToSeparatedString()}");
63-
return _context.Tracker.LoadWorkItems(ids, (workItemIds) =>
64+
var accumulator = new List<WorkItemWrapper>();
65+
66+
// prevent VS403474: You requested nnn work items which exceeds the limit of 200
67+
foreach (var idBlock in ids.Paginate(VS403474_LIMIT))
6468
{
65-
_context.Logger.WriteInfo($"Loading workitems {workItemIds.ToSeparatedString()}");
66-
var items = _clients.WitClient.GetWorkItemsAsync(workItemIds, expand: WorkItemExpand.All).Result;
67-
return items.ConvertAll(i => new WorkItemWrapper(_context, i));
68-
});
69+
_context.Logger.WriteVerbose($"Getting workitems {idBlock.ToSeparatedString()}");
70+
var workItemBlock = _context.Tracker.LoadWorkItems(idBlock, (workItemIds) =>
71+
{
72+
_context.Logger.WriteInfo($"Loading workitems {workItemIds.ToSeparatedString()}");
73+
var items = _clients.WitClient.GetWorkItemsAsync(workItemIds, expand: WorkItemExpand.All).Result;
74+
return items.ConvertAll(i => new WorkItemWrapper(_context, i));
75+
});
76+
77+
accumulator.AddRange(workItemBlock);
78+
}
79+
80+
return accumulator;
6981
}
7082

7183
public IList<WorkItemWrapper> GetWorkItems(IEnumerable<WorkItemRelationWrapper> collection)
@@ -543,4 +555,4 @@ public class BacklogInfo
543555
public string Name { get; set; }
544556
}
545557

546-
}
558+
}

src/integrationtests-cli/Directory.Build.targets

-9
This file was deleted.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"subscription": "${SubscriptionID}",
3+
"client": "${ClientID}",
4+
"password": "${Password}",
5+
"tenant": "${TenantID}",
6+
"location": "${Location}",
7+
"resourceGroup": "${ResourceGroup}",
8+
"uniqueSuffix": "",
9+
"devopsUrl": "${DevOpsUrl}",
10+
"pat": "${PAT}",
11+
"projectName": "${ProjectName}",
12+
"runtimeSourceUrl": "${RuntimeSourceUrl}"
13+
}
14+

src/integrationtests-setup/main.tf

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
### MAIN
2+
3+
resource "azuread_application" "integration_tests" {
4+
name = "AggregatorIntegrationTests"
5+
}
6+
7+
resource "azuread_service_principal" "integration_tests" {
8+
application_id = azuread_application.integration_tests.application_id
9+
app_role_assignment_required = false
10+
}
11+
12+
// we need to output this, so cannot
13+
resource "random_password" "service_principal_password" {
14+
keepers = {
15+
# Generate a new id each time we switch to a new AAD application
16+
app_id = azuread_application.integration_tests.application_id
17+
}
18+
19+
length = 24
20+
special = true
21+
override_special = ".!-_=+"
22+
}
23+
24+
locals {
25+
hours_in_a_year = 24 * 365.25
26+
one_year_interval = format("%dh", local.hours_in_a_year)
27+
a_year_from_now = timeadd(timestamp(), local.one_year_interval)
28+
}
29+
30+
resource "azuread_service_principal_password" "integration_tests" {
31+
service_principal_id = azuread_service_principal.integration_tests.id
32+
value = random_password.service_principal_password.result
33+
end_date = "2099-01-01T01:02:03Z" //local.a_year_from_now
34+
}
35+
36+
resource "azurerm_resource_group" "integration_tests" {
37+
name = "aggregator-integration-tests"
38+
location = var.resource_group_location
39+
40+
tags = {
41+
source = "aggregator"
42+
}
43+
}
44+
45+
resource "azurerm_resource_group" "integration_tests_scenario4" {
46+
name = "aggregator-test-gv1"
47+
location = var.resource_group_location
48+
49+
tags = {
50+
source = "aggregator"
51+
}
52+
}
53+
54+
resource "azurerm_role_assignment" "integration_tests" {
55+
scope = azurerm_resource_group.integration_tests.id
56+
role_definition_name = "Contributor"
57+
principal_id = azuread_service_principal.integration_tests.id
58+
}
59+
60+
resource "azurerm_role_assignment" "integration_tests_scenario4" {
61+
scope = azurerm_resource_group.integration_tests_scenario4.id
62+
role_definition_name = "Contributor"
63+
principal_id = azuread_service_principal.integration_tests.id
64+
}
65+
66+
resource "azuredevops_project" "integration_tests" {
67+
project_name = "AggregatorIntegrationTests"
68+
description = "Integration test for Aggregator CLI"
69+
}
70+
71+
# EOF
72+

src/integrationtests-setup/output.tf

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
data "azurerm_client_config" "current" {}
2+
data "azuredevops_client_config" "current" {}
3+
4+
locals {
5+
defaultRuntimePath = "${path.module}/../aggregator-cli/FunctionRuntime.zip"
6+
}
7+
8+
resource "local_file" "logon_data" {
9+
sensitive_content = templatefile("${path.module}/logon-data.tmpl",
10+
{
11+
SubscriptionID = data.azurerm_client_config.current.subscription_id,
12+
TenantID = data.azurerm_client_config.current.tenant_id,
13+
DisplayName = azuread_service_principal.integration_tests.display_name,
14+
ObjectID = azuread_service_principal.integration_tests.object_id,
15+
ApplicationID = azuread_service_principal.integration_tests.application_id,
16+
ClientID = azuread_service_principal.integration_tests.application_id, // alias
17+
Password = random_password.service_principal_password.result,
18+
ResourceGroup = azurerm_resource_group.integration_tests.name,
19+
Location = azurerm_resource_group.integration_tests.location,
20+
DevOpsUrl = data.azuredevops_client_config.current.organization_url,
21+
ProjectName = azuredevops_project.integration_tests.project_name,
22+
PAT = var.azdo_personal_access_token,
23+
RuntimeSourceUrl = "file://${abspath(local.defaultRuntimePath)}"
24+
})
25+
filename = "${path.module}/../integrationtests-cli/logon-data.json"
26+
file_permission = "0640"
27+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
terraform {
2+
required_version = "~> 0.12"
3+
}
4+
5+
provider "random" {
6+
version = "~> 2.2"
7+
}
8+
9+
provider "local" {
10+
version = "~> 1.4"
11+
}
12+
13+
provider "azuread" {
14+
version = "~>0.7.0"
15+
}
16+
17+
provider "azurerm" {
18+
version = "~>2.0.0"
19+
features {}
20+
// to logon use `az logon`
21+
# subscription_id = var.azurerm_subscription_id
22+
# client_id = var.azurerm_client_id
23+
# client_secret = var.azurerm_client_secret
24+
# tenant_id = var.azurerm_tenant_id
25+
}
26+
27+
provider "azuredevops" {
28+
version = ">= 0.0.1"
29+
org_service_url = "https://dev.azure.com/giuliovaad"
30+
personal_access_token = var.azdo_personal_access_token
31+
}
32+
33+
# EOF
34+
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### VARIABLES
2+
3+
# variable "azurerm_subscription_id" {}
4+
# variable "azurerm_client_id" {}
5+
# variable "azurerm_client_secret" {}
6+
# variable "azurerm_tenant_id" {}
7+
8+
# this must be a variable as it will be saved in the generated json file
9+
variable "azdo_personal_access_token" {}
10+
11+
variable "resource_group_location" {
12+
default = "westeurope"
13+
}
14+
15+
# EOF #
16+

src/unittests-ruleng/WorkItemStoreTests.cs

+75
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,81 @@ public void GetWorkItems_ByIds_Succeeds()
8383
Assert.Contains(wis, (x) => x.Id.Value == 99);
8484
}
8585

86+
List<WorkItem> GenerateWorkItems(int startId, int count = 200)
87+
{
88+
return Enumerable
89+
.Range(startId, count)
90+
.Select(i => new WorkItem
91+
{
92+
Id = i,
93+
Fields = new Dictionary<string, object>()
94+
}).ToList();
95+
}
96+
97+
[Fact]
98+
public void GetWorkItems_ByIds_LessThan200_Succeeds()
99+
{
100+
witClient.GetWorkItemsAsync(Arg.Any<IEnumerable<int>>(), expand: WorkItemExpand.All)
101+
.Returns(
102+
GenerateWorkItems(1, 199)
103+
);
104+
var ids = Enumerable.Range(1, 199).ToArray();
105+
106+
var context = new EngineContext(clientsContext, clientsContext.ProjectId, clientsContext.ProjectName, logger, new RuleSettings());
107+
var sut = new WorkItemStore(context);
108+
109+
var wis = sut.GetWorkItems(ids);
110+
111+
Assert.NotEmpty(wis);
112+
Assert.Equal(ids.Length, wis.Count);
113+
Assert.Contains(wis, (x) => x.Id.Value == 42);
114+
Assert.Contains(wis, (x) => x.Id.Value == 199);
115+
}
116+
117+
[Fact]
118+
public void GetWorkItems_ByIds_MoreThan200_Succeeds()
119+
{
120+
witClient.GetWorkItemsAsync(Arg.Any<IEnumerable<int>>(), expand: WorkItemExpand.All)
121+
.Returns(
122+
GenerateWorkItems(1, 200),
123+
GenerateWorkItems(201, 150)
124+
);
125+
var ids = Enumerable.Range(1, 350).ToArray();
126+
127+
var context = new EngineContext(clientsContext, clientsContext.ProjectId, clientsContext.ProjectName, logger, new RuleSettings());
128+
var sut = new WorkItemStore(context);
129+
130+
var wis = sut.GetWorkItems(ids);
131+
132+
Assert.NotEmpty(wis);
133+
Assert.Equal(ids.Length, wis.Count);
134+
Assert.Contains(wis, (x) => x.Id.Value == 42);
135+
Assert.Contains(wis, (x) => x.Id.Value == 299);
136+
}
137+
138+
[Fact]
139+
public void GetWorkItems_ByIds_MoreThan400_Succeeds()
140+
{
141+
witClient.GetWorkItemsAsync(Arg.Any<IEnumerable<int>>(), expand: WorkItemExpand.All)
142+
.Returns(
143+
GenerateWorkItems(1, 200),
144+
GenerateWorkItems(201, 200),
145+
GenerateWorkItems(401, 33)
146+
);
147+
var ids = Enumerable.Range(1, 433).ToArray();
148+
149+
var context = new EngineContext(clientsContext, clientsContext.ProjectId, clientsContext.ProjectName, logger, new RuleSettings());
150+
var sut = new WorkItemStore(context);
151+
152+
var wis = sut.GetWorkItems(ids);
153+
154+
Assert.NotEmpty(wis);
155+
Assert.Equal(ids.Length, wis.Count);
156+
Assert.Contains(wis, (x) => x.Id.Value == 42);
157+
Assert.Contains(wis, (x) => x.Id.Value == 299);
158+
Assert.Contains(wis, (x) => x.Id.Value == 410);
159+
}
160+
86161
[Fact]
87162
public async Task NewWorkItem_Succeeds()
88163
{

0 commit comments

Comments
 (0)