Skip to content

Commit 5d65c06

Browse files
committed
adding a test for HttpTriggers and disposed hosts; fixing issue with 'functions' whitelist; improving unrelated tests
1 parent b27267e commit 5d65c06

File tree

7 files changed

+220
-80
lines changed

7 files changed

+220
-80
lines changed

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,10 @@ internal static void ApplyConfiguration(JObject config, ScriptHostConfiguration
11231123
scriptConfig.Functions.Add((string)function);
11241124
}
11251125
}
1126+
else
1127+
{
1128+
scriptConfig.Functions = null;
1129+
}
11261130

11271131
// We may already have a host id, but the one from the JSON takes precedence
11281132
JToken hostId = (JToken)config["id"];

test/WebJobs.Script.Tests.Integration/Host/WebScriptHostManagerTests.cs

Lines changed: 9 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Collections.ObjectModel;
76
using System.Diagnostics;
87
using System.IO;
98
using System.Linq;
@@ -31,9 +30,6 @@ public class WebScriptHostManagerTests : IClassFixture<WebScriptHostManagerTests
3130
private readonly TempDirectory _secretsDirectory = new TempDirectory();
3231
private WebScriptHostManagerTests.Fixture _fixture;
3332

34-
// Some tests need their own manager that differs from the fixture.
35-
private WebScriptHostManager _manager;
36-
3733
public WebScriptHostManagerTests(WebScriptHostManagerTests.Fixture fixture)
3834
{
3935
_fixture = fixture;
@@ -53,11 +49,15 @@ public async Task FunctionInvoke_SystemTraceEventsAreEmitted()
5349
};
5450
await host.CallAsync("ManualTrigger", parameters);
5551

52+
// it's possible that the TimerTrigger fires during this so filter them out.
53+
54+
string[] events = _fixture.EventGenerator.Events.Where(e => !e.Contains("TimerTrigger")).ToArray();
55+
Assert.True(events.Length == 4, $"Expected 4 events. Actual: {events.Length}. Actual events: {Environment.NewLine}{string.Join(Environment.NewLine, events)}");
5656
Assert.Equal(4, _fixture.EventGenerator.Events.Count);
57-
Assert.True(_fixture.EventGenerator.Events[0].StartsWith("Info WebJobs.Execution Executing 'Functions.ManualTrigger' (Reason='This function was programmatically called via the host APIs.', Id="));
58-
Assert.True(_fixture.EventGenerator.Events[1].StartsWith("Info ManualTrigger Function started (Id="));
59-
Assert.True(_fixture.EventGenerator.Events[2].StartsWith("Info ManualTrigger Function completed (Success, Id="));
60-
Assert.True(_fixture.EventGenerator.Events[3].StartsWith("Info WebJobs.Execution Executed 'Functions.ManualTrigger' (Succeeded, Id="));
57+
Assert.StartsWith("Info WebJobs.Execution Executing 'Functions.ManualTrigger' (Reason='This function was programmatically called via the host APIs.', Id=", events[0]);
58+
Assert.StartsWith("Info ManualTrigger Function started (Id=", events[1]);
59+
Assert.StartsWith("Info ManualTrigger Function completed (Success, Id=", events[2]);
60+
Assert.StartsWith("Info WebJobs.Execution Executed 'Functions.ManualTrigger' (Succeeded, Id=", events[3]);
6161

6262
// make sure the user log wasn't traced
6363
Assert.False(_fixture.EventGenerator.Events.Any(p => p.Contains("ManualTrigger function invoked!")));
@@ -118,7 +118,7 @@ public async Task EmptyHost_StartsSuccessfully()
118118
webHostSettings.SecretsPath = _secretsDirectory.Path;
119119
var mockEventManager = new Mock<IScriptEventManager>();
120120

121-
ScriptHostManager hostManager = new WebScriptHostManager(config, new TestSecretManagerFactory(secretManager), mockEventManager.Object, _settingsManager, webHostSettings);
121+
ScriptHostManager hostManager = new WebScriptHostManager(config, new TestSecretManagerFactory(secretManager), mockEventManager.Object, _settingsManager, webHostSettings);
122122

123123
Task runTask = Task.Run(() => hostManager.RunAndBlock());
124124

@@ -186,33 +186,6 @@ await TestHelpers.Await(() =>
186186
Assert.Equal(typeof(SystemTraceWriter), config.TraceWriter.GetType());
187187
}
188188

189-
[Fact]
190-
public async Task OnTimeoutException_IgnoreToken_StopsManager()
191-
{
192-
var trace = new TestTraceWriter(TraceLevel.Info);
193-
194-
await RunTimeoutExceptionTest(trace, handleCancellation: false);
195-
196-
await TestHelpers.Await(() => !(_manager.State == ScriptHostState.Running));
197-
Assert.DoesNotContain(trace.Traces, t => t.Message.StartsWith("Done"));
198-
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Timeout value of 00:00:03 exceeded by function 'Functions.TimeoutToken' (Id: "));
199-
Assert.Contains(trace.Traces, t => t.Message == "A function timeout has occurred. Host is shutting down.");
200-
}
201-
202-
[Fact]
203-
public async Task OnTimeoutException_UsesToken_ManagerKeepsRunning()
204-
{
205-
var trace = new TestTraceWriter(TraceLevel.Info);
206-
207-
await RunTimeoutExceptionTest(trace, handleCancellation: true);
208-
209-
// wait a few seconds to make sure the manager doesn't die
210-
await Assert.ThrowsAsync<ApplicationException>(() => TestHelpers.Await(() => !(_manager.State == ScriptHostState.Running), timeout: 3000));
211-
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Done"));
212-
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Timeout value of 00:00:03 exceeded by function 'Functions.TimeoutToken' (Id: "));
213-
Assert.DoesNotContain(trace.Traces, t => t.Message == "A function timeout has occurred. Host is shutting down.");
214-
}
215-
216189
[Fact]
217190
public void AddRouteDataToRequest_DoesNotAddRequestProperty_WhenRouteDataNull()
218191
{
@@ -249,50 +222,8 @@ public void AddRouteDataToRequest_AddsRequestProperty_WhenRouteDataNotNull()
249222
Assert.Equal(result["p4"], null);
250223
}
251224

252-
private async Task RunTimeoutExceptionTest(TraceWriter trace, bool handleCancellation)
253-
{
254-
TimeSpan gracePeriod = TimeSpan.FromMilliseconds(5000);
255-
_manager = await CreateAndStartWebScriptHostManager(trace);
256-
257-
string scenarioName = handleCancellation ? "useToken" : "ignoreToken";
258-
259-
var args = new Dictionary<string, object>
260-
{
261-
{ "input", scenarioName }
262-
};
263-
264-
await Assert.ThrowsAsync<FunctionTimeoutException>(() => _manager.Instance.CallAsync("TimeoutToken", args));
265-
}
266-
267-
private async Task<WebScriptHostManager> CreateAndStartWebScriptHostManager(TraceWriter traceWriter)
268-
{
269-
var functions = new Collection<string> { "TimeoutToken" };
270-
271-
ScriptHostConfiguration config = new ScriptHostConfiguration()
272-
{
273-
RootScriptPath = $@"TestScripts\CSharp",
274-
TraceWriter = traceWriter,
275-
FileLoggingMode = FileLoggingMode.Always,
276-
Functions = functions,
277-
FunctionTimeout = TimeSpan.FromSeconds(3)
278-
};
279-
280-
var mockEventManager = new Mock<IScriptEventManager>();
281-
var manager = new WebScriptHostManager(config, new TestSecretManagerFactory(), mockEventManager.Object, _settingsManager, new WebHostSettings { SecretsPath = _secretsDirectory.Path });
282-
Task task = Task.Run(() => { manager.RunAndBlock(); });
283-
await TestHelpers.Await(() => manager.State == ScriptHostState.Running);
284-
285-
return manager;
286-
}
287-
288225
public void Dispose()
289226
{
290-
if (_manager != null)
291-
{
292-
_manager.Stop();
293-
_manager.Dispose();
294-
}
295-
296227
_secretsDirectory.Dispose();
297228
}
298229

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using System.Diagnostics;
8+
using System.Threading.Tasks;
9+
using Microsoft.Azure.WebJobs.Host;
10+
using Microsoft.Azure.WebJobs.Script.Config;
11+
using Microsoft.Azure.WebJobs.Script.Eventing;
12+
using Microsoft.Azure.WebJobs.Script.WebHost;
13+
using Moq;
14+
using WebJobs.Script.Tests;
15+
using Xunit;
16+
17+
namespace Microsoft.Azure.WebJobs.Script.Tests.Host
18+
{
19+
public class WebScriptHostManagerTimeoutTests : IDisposable
20+
{
21+
private readonly TempDirectory _secretsDirectory = new TempDirectory();
22+
private WebScriptHostManager _manager;
23+
24+
[Fact]
25+
public async Task OnTimeoutException_IgnoreToken_StopsManager()
26+
{
27+
var trace = new TestTraceWriter(TraceLevel.Info);
28+
29+
await RunTimeoutExceptionTest(trace, handleCancellation: false);
30+
31+
await TestHelpers.Await(() => !(_manager.State == ScriptHostState.Running));
32+
Assert.DoesNotContain(trace.Traces, t => t.Message.StartsWith("Done"));
33+
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Timeout value of 00:00:03 exceeded by function 'Functions.TimeoutToken' (Id: "));
34+
Assert.Contains(trace.Traces, t => t.Message == "A function timeout has occurred. Host is shutting down.");
35+
}
36+
37+
[Fact]
38+
public async Task OnTimeoutException_UsesToken_ManagerKeepsRunning()
39+
{
40+
var trace = new TestTraceWriter(TraceLevel.Info);
41+
42+
await RunTimeoutExceptionTest(trace, handleCancellation: true);
43+
44+
// wait a few seconds to make sure the manager doesn't die
45+
await Assert.ThrowsAsync<ApplicationException>(() => TestHelpers.Await(() => !(_manager.State == ScriptHostState.Running),
46+
timeout: 3000, throwWhenDebugging: true));
47+
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Done"));
48+
Assert.Contains(trace.Traces, t => t.Message.StartsWith("Timeout value of 00:00:03 exceeded by function 'Functions.TimeoutToken' (Id: "));
49+
Assert.DoesNotContain(trace.Traces, t => t.Message == "A function timeout has occurred. Host is shutting down.");
50+
}
51+
52+
private async Task RunTimeoutExceptionTest(TraceWriter trace, bool handleCancellation)
53+
{
54+
TimeSpan gracePeriod = TimeSpan.FromMilliseconds(5000);
55+
_manager = await CreateAndStartWebScriptHostManager(trace);
56+
57+
string scenarioName = handleCancellation ? "useToken" : "ignoreToken";
58+
59+
var args = new Dictionary<string, object>
60+
{
61+
{ "input", scenarioName }
62+
};
63+
64+
await Assert.ThrowsAsync<FunctionTimeoutException>(() => _manager.Instance.CallAsync("TimeoutToken", args));
65+
}
66+
67+
private async Task<WebScriptHostManager> CreateAndStartWebScriptHostManager(TraceWriter traceWriter)
68+
{
69+
var functions = new Collection<string> { "TimeoutToken" };
70+
71+
ScriptHostConfiguration config = new ScriptHostConfiguration()
72+
{
73+
RootScriptPath = $@"TestScripts\CSharp",
74+
TraceWriter = traceWriter,
75+
FileLoggingMode = FileLoggingMode.Always,
76+
Functions = functions,
77+
FunctionTimeout = TimeSpan.FromSeconds(3)
78+
};
79+
80+
var mockEventManager = new Mock<IScriptEventManager>();
81+
var manager = new WebScriptHostManager(config, new TestSecretManagerFactory(), mockEventManager.Object, ScriptSettingsManager.Instance, new WebHostSettings { SecretsPath = _secretsDirectory.Path });
82+
Task task = Task.Run(() => { manager.RunAndBlock(); });
83+
await TestHelpers.Await(() => manager.State == ScriptHostState.Running);
84+
85+
return manager;
86+
}
87+
88+
public void Dispose()
89+
{
90+
if (_manager != null)
91+
{
92+
_manager.Stop();
93+
_manager.Dispose();
94+
}
95+
96+
_secretsDirectory.Dispose();
97+
}
98+
}
99+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Net.Http;
8+
using System.Threading.Tasks;
9+
using Newtonsoft.Json.Linq;
10+
using Xunit;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Tests
13+
{
14+
// Running these in their own class with private fixtures so they do not affect the timing of
15+
// subsequent tests.
16+
public class SamplesEndToEndRestartTests
17+
{
18+
[Fact(Skip = "Will not pass until https://github.com/Azure/azure-webjobs-sdk-script/issues/1537 is fixed")]
19+
public async Task HttpTrigger_HostRestarts()
20+
{
21+
string uri = "api/httptrigger-csharp?name=brett";
22+
23+
bool stop = false;
24+
bool failed = false;
25+
HttpResponseMessage failedResponse = null;
26+
27+
// Use a private fixture to prevent timing restart issues with other tests
28+
SamplesEndToEndTests.TestFixture fixture = new SamplesEndToEndTests.TestFixture();
29+
string hostJsonPath = Path.Combine(fixture.HostSettings.ScriptPath, "host.json");
30+
string originalHostJson = File.ReadAllText(hostJsonPath);
31+
try
32+
{
33+
// setup just this function to keep things quicker
34+
JObject hostJson = JObject.Parse(originalHostJson);
35+
hostJson["functions"] = new JArray("HttpTrigger-CSharp");
36+
File.WriteAllText(hostJsonPath, hostJson.ToString());
37+
38+
List<Task> requestTasks = new List<Task>();
39+
40+
for (int i = 0; i < 5; i++)
41+
{
42+
requestTasks.Add(Task.Run(() =>
43+
{
44+
while (!failed && !stop)
45+
{
46+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
47+
request.Headers.Add("x-functions-key", "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy");
48+
HttpResponseMessage response = fixture.HttpClient.SendAsync(request).Result;
49+
if (!response.IsSuccessStatusCode)
50+
{
51+
failed = true;
52+
failedResponse = response;
53+
}
54+
}
55+
}));
56+
}
57+
58+
string functionJsonPath = Path.Combine(fixture.HostSettings.ScriptPath, "HttpTrigger-CSharp", "function.json");
59+
60+
// try for 2 minutes to force a host restart
61+
for (int i = 0; i < 60 && !failed; i++)
62+
{
63+
await Task.Delay(2000);
64+
File.SetLastWriteTimeUtc(functionJsonPath, DateTime.UtcNow);
65+
}
66+
67+
stop = true;
68+
await Task.WhenAll(requestTasks);
69+
70+
Assert.False(failed, $"Request failed. Response: {failedResponse?.StatusCode} {failedResponse?.Content.ReadAsStringAsync().Result}");
71+
}
72+
finally
73+
{
74+
File.WriteAllText(hostJsonPath, originalHostJson);
75+
fixture.Dispose();
76+
}
77+
}
78+
}
79+
}

test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@
405405
<Compile Include="BashEndToEndTests.cs" />
406406
<Compile Include="BlobLeaseManagerTests.cs" />
407407
<Compile Include="GlobalSuppressions.cs" />
408+
<Compile Include="Host\WebScriptHostManagerTimeoutTests.cs" />
408409
<Compile Include="NodeContentTests.cs" />
409410
<Compile Include="Controllers\ControllerScenarioTestFixture.cs" />
410411
<Compile Include="Controllers\Keys\DeleteHostFunctionKeysScenario.cs" />
@@ -430,6 +431,7 @@
430431
<DesignTime>True</DesignTime>
431432
<DependentUpon>Resources.resx</DependentUpon>
432433
</Compile>
434+
<Compile Include="SamplesEndToEndRestartTests.cs" />
433435
<Compile Include="SamplesEndToEndTests.cs" />
434436
<Compile Include="Host\ScriptHostManagerTests.cs" />
435437
<Compile Include="Host\StandbyModeTests.cs" />

test/WebJobs.Script.Tests.Shared/TestHelpers.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ public static string FunctionsTestDirectory
3131
}
3232
}
3333

34-
public static async Task Await(Func<bool> condition, int timeout = 60 * 1000, int pollingInterval = 2 * 1000)
34+
public static async Task Await(Func<bool> condition, int timeout = 60 * 1000, int pollingInterval = 2 * 1000, bool throwWhenDebugging = false)
3535
{
3636
DateTime start = DateTime.Now;
3737
while (!condition())
3838
{
3939
await Task.Delay(pollingInterval);
4040

41-
if (!Debugger.IsAttached && (DateTime.Now - start).TotalMilliseconds > timeout)
41+
bool shouldThrow = !Debugger.IsAttached || (Debugger.IsAttached && throwWhenDebugging);
42+
if (shouldThrow && (DateTime.Now - start).TotalMilliseconds > timeout)
4243
{
4344
throw new ApplicationException("Condition not reached within timeout.");
4445
}

test/WebJobs.Script.Tests/ScriptHostTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,30 @@ public void ApplyConfiguration_AppliesFunctionsFilter()
681681
Assert.Equal("Function2", scriptConfig.Functions.ElementAt(1));
682682
}
683683

684+
[Fact]
685+
public void ApplyConfiguration_ClearsFunctionsFilter()
686+
{
687+
// A previous bug wouldn't properly clear the filter if you removed it.
688+
JObject config = new JObject();
689+
config["id"] = ID;
690+
691+
ScriptHostConfiguration scriptConfig = new ScriptHostConfiguration();
692+
Assert.Null(scriptConfig.Functions);
693+
694+
config["functions"] = new JArray("Function1", "Function2");
695+
696+
ScriptHost.ApplyConfiguration(config, scriptConfig);
697+
Assert.Equal(2, scriptConfig.Functions.Count);
698+
Assert.Equal("Function1", scriptConfig.Functions.ElementAt(0));
699+
Assert.Equal("Function2", scriptConfig.Functions.ElementAt(1));
700+
701+
config.Remove("functions");
702+
703+
ScriptHost.ApplyConfiguration(config, scriptConfig);
704+
705+
Assert.Null(scriptConfig.Functions);
706+
}
707+
684708
[Fact]
685709
public void ApplyConfiguration_AppliesTimeout()
686710
{

0 commit comments

Comments
 (0)