@@ -41,104 +41,121 @@ public ScriptHostManagerTests()
4141 // Update a script file (the function.json) to force the ScriptHost to re-index and pick up new changes.
4242 // Test with timers:
4343 [ Fact ]
44- public async Task UpdateFileAndRestart ( )
44+ public void UpdateFileAndRestart ( )
4545 {
46- CancellationTokenSource cts = new CancellationTokenSource ( ) ;
47- var fixture = new NodeEndToEndTests . TestFixture ( ) ;
48- var blob1 = UpdateOutputName ( "testblob" , "first" , fixture ) ;
49-
50- await fixture . Host . StopAsync ( ) ;
46+ var fixture = new NodeEndToEndTests . TestFixture ( false ) ;
5147 var config = fixture . Host . ScriptConfig ;
5248
53- ExceptionDispatchInfo exception = null ;
54- ExceptionDispatchInfo updateException = null ;
49+ config . OnConfigurationApplied = c =>
50+ {
51+ c . Functions = new Collection < string > { "TimerTrigger" } ;
52+ } ;
53+
54+ var blob1 = UpdateOutputName ( "testblob" , "first" , fixture ) ;
55+
5556 using ( var eventManager = new ScriptEventManager ( ) )
5657 using ( var manager = new ScriptHostManager ( config , eventManager ) )
5758 {
59+ string GetErrorTraces ( )
60+ {
61+ var messages = fixture . TraceWriter . GetTraces ( )
62+ . Where ( t => t . Level == TraceLevel . Error )
63+ . Select ( t => t . Message ) ;
64+
65+ return string . Join ( Environment . NewLine , messages ) ;
66+ }
67+
68+ List < Exception > exceptions = new List < Exception > ( ) ;
69+
5870 // Background task to run while the main thread is pumping events at RunAndBlock().
59- Thread t = new Thread ( _ =>
71+ Thread backgroundThread = new Thread ( _ =>
6072 {
6173 try
6274 {
6375 // don't start until the manager is running
64- TestHelpers . Await ( ( ) => manager . State == ScriptHostState . Running ) . Wait ( ) ;
76+ TestHelpers . Await ( ( ) => manager . State == ScriptHostState . Running ,
77+ userMessageCallback : GetErrorTraces ) . GetAwaiter ( ) . GetResult ( ) ;
6578
6679 // Wait for initial execution.
6780 TestHelpers . Await ( ( ) =>
68- {
69- bool exists = blob1 . Exists ( ) ;
70- return exists ;
71- } , timeout : 10 * 1000 ) . Wait ( ) ;
81+ {
82+ bool exists = blob1 . Exists ( ) ;
83+ return exists ;
84+ } , timeout : 10 * 1000 , userMessageCallback : GetErrorTraces ) . GetAwaiter ( ) . GetResult ( ) ;
7285
7386 // This changes the bindings so that we now write to blob2
7487 var blob2 = UpdateOutputName ( "first" , "testblob" , fixture ) ;
7588
7689 // wait for newly executed
7790 TestHelpers . Await ( ( ) =>
78- {
79- bool exists = blob2 . Exists ( ) ;
80- return exists ;
81- } , timeout : 30 * 1000 ) . Wait ( ) ;
91+ {
92+ bool exists = blob2 . Exists ( ) ;
93+ return exists ;
94+ } , timeout : 30 * 1000 , userMessageCallback : GetErrorTraces ) . GetAwaiter ( ) . GetResult ( ) ;
95+
96+ // The TimerTrigger can fire before the host is fully started. To be more
97+ // reliably clean up the test, wait until it is running before calling Stop.
98+ TestHelpers . Await ( ( ) => manager . State == ScriptHostState . Running ,
99+ userMessageCallback : GetErrorTraces ) . GetAwaiter ( ) . GetResult ( ) ;
82100 }
83101 catch ( Exception ex )
84102 {
85- exception = ExceptionDispatchInfo . Capture ( ex ) ;
103+ exceptions . Add ( ex ) ;
86104 }
87105 finally
88106 {
89107 try
90108 {
91- UpdateOutputName ( "first" , "testblob" , fixture ) ;
109+ // Calling Stop (rather than using a token) lets us wait until all listeners have stopped.
110+ manager . Stop ( ) ;
92111 }
93112 catch ( Exception ex )
94113 {
95- updateException = ExceptionDispatchInfo . Capture ( ex ) ;
114+ exceptions . Add ( ex ) ;
96115 }
97116 }
98-
99- cts . Cancel ( ) ;
100117 } ) ;
101- t . Start ( ) ;
102-
103- manager . RunAndBlock ( cts . Token ) ;
104118
105- t . Join ( ) ;
119+ try
120+ {
121+ backgroundThread . Start ( ) ;
122+ manager . RunAndBlock ( ) ;
123+ Assert . True ( backgroundThread . Join ( 60000 ) , "The background task did not complete in 60 seconds." ) ;
106124
107- Assert . True ( exception == null , exception ? . SourceException ? . ToString ( ) ) ;
108- Assert . True ( updateException == null , "Failed to update output name: " + updateException ? . SourceException ? . ToString ( ) ) ;
125+ string exceptionString = string . Join ( Environment . NewLine , exceptions . Select ( p => p . ToString ( ) ) ) ;
126+ Assert . True ( exceptions . Count ( ) == 0 , exceptionString ) ;
127+ }
128+ finally
129+ {
130+ // make sure to put the original names back
131+ UpdateOutputName ( "first" , "testblob" , fixture ) ;
132+ }
109133 }
110134 }
111135
112136 [ Fact ]
113- public async Task RenameFunctionAndRestart ( )
137+ public void RenameFunctionAndRestart ( )
114138 {
115139 var oldDirectory = Path . Combine ( Directory . GetCurrentDirectory ( ) , "TestScripts/Node/TimerTrigger" ) ;
116140 var newDirectory = Path . Combine ( Directory . GetCurrentDirectory ( ) , "TestScripts/Node/MovedTrigger" ) ;
117141
118- CancellationTokenSource cts = new CancellationTokenSource ( ) ;
142+ var fixture = new NodeEndToEndTests . TestFixture ( false ) ;
143+ var config = fixture . Host . ScriptConfig ;
119144
120- // Don't wait forever
121- bool timeout = false ;
122- Task ignore = Task . Delay ( TimeSpan . FromMinutes ( 3 ) ) . ContinueWith ( ( t ) =>
145+ config . OnConfigurationApplied = c =>
123146 {
124- timeout = true ;
125- cts . Cancel ( ) ;
126- } ) ;
127-
128- var fixture = new NodeEndToEndTests . TestFixture ( ) ;
129- await fixture . Host . StopAsync ( ) ;
130- var config = fixture . Host . ScriptConfig ;
131- List < string > pollMessages = new List < string > ( ) ;
147+ c . Functions = new Collection < string > { "TimerTrigger" , "MovedTrigger" } ;
148+ } ;
132149
133150 var blob = fixture . TestOutputContainer . GetBlockBlobReference ( "testblob" ) ;
134-
135- ExceptionDispatchInfo exception = null ;
136- ExceptionDispatchInfo moveException = null ;
137151 var mockEnvironment = new Mock < IScriptHostEnvironment > ( ) ;
152+
138153 using ( var eventManager = new ScriptEventManager ( ) )
139154 using ( var manager = new ScriptHostManager ( config , eventManager , mockEnvironment . Object ) )
140155 using ( var resetEvent = new ManualResetEventSlim ( ) )
141156 {
157+ List < Exception > exceptions = new List < Exception > ( ) ;
158+
142159 mockEnvironment . Setup ( e => e . RestartHost ( ) )
143160 . Callback ( ( ) =>
144161 {
@@ -147,21 +164,21 @@ public async Task RenameFunctionAndRestart()
147164 } ) ;
148165
149166 // Background task to run while the main thread is pumping events at RunAndBlock().
150- Thread t = new Thread ( _ =>
167+ Thread backgroundThread = new Thread ( _ =>
151168 {
152169 try
153170 {
154171 // don't start until the manager is running
155172 TestHelpers . Await ( ( ) => manager . State == ScriptHostState . Running ,
156- userMessageCallback : ( ) => "Host did not start in time." ) . Wait ( ) ;
173+ userMessageCallback : ( ) => "Host did not start in time." ) . GetAwaiter ( ) . GetResult ( ) ;
157174
158175 // Wait for initial execution.
159176 TestHelpers . Await ( ( ) =>
160177 {
161178 bool exists = blob . Exists ( ) ;
162- pollMessages . Add ( $ "TimerTrigger: [{ DateTime . UtcNow . ToString ( "HH:mm:ss.fff" ) } ] '{ blob . Uri } ' exists: { exists } ") ;
163179 return exists ;
164- } , timeout : 10 * 1000 , userMessageCallback : ( ) => $ "Blob '{ blob . Uri } ' was not created by 'TimerTrigger' in time.") . Wait ( ) ;
180+ } , timeout : 10 * 1000 ,
181+ userMessageCallback : ( ) => $ "Blob '{ blob . Uri } ' was not created by 'TimerTrigger' in time.") . GetAwaiter ( ) . GetResult ( ) ;
165182
166183 // find __dirname from blob
167184 string text ;
@@ -184,9 +201,9 @@ public async Task RenameFunctionAndRestart()
184201 TestHelpers . Await ( ( ) =>
185202 {
186203 bool exists = blob . Exists ( ) ;
187- pollMessages . Add ( $ "MovedTrigger: [{ DateTime . UtcNow . ToString ( "HH:mm:ss.fff" ) } ] '{ blob . Uri } ' exists: { exists } ") ;
188204 return exists ;
189- } , timeout : 30 * 1000 , userMessageCallback : ( ) => $ "Blob '{ blob . Uri } ' was not created by 'MovedTrigger' in time.") . Wait ( ) ;
205+ } , timeout : 30 * 1000 ,
206+ userMessageCallback : ( ) => $ "Blob '{ blob . Uri } ' was not created by 'MovedTrigger' in time.") . GetAwaiter ( ) . GetResult ( ) ;
190207
191208 using ( var stream = new MemoryStream ( ) )
192209 {
@@ -195,36 +212,46 @@ public async Task RenameFunctionAndRestart()
195212 }
196213
197214 Assert . Contains ( "MovedTrigger" , text ) ;
215+
216+ // The TimerTrigger can fire before the host is fully started. To be more
217+ // reliably clean up the test, wait until it is running before calling Stop.
218+ TestHelpers . Await ( ( ) => manager . State == ScriptHostState . Running ) . GetAwaiter ( ) . GetResult ( ) ;
198219 }
199220 catch ( Exception ex )
200221 {
201- exception = ExceptionDispatchInfo . Capture ( ex ) ;
222+ exceptions . Add ( ex ) ;
202223 }
203224 finally
204225 {
205226 try
206227 {
207- Directory . Move ( newDirectory , oldDirectory ) ;
228+ manager . Stop ( ) ;
208229 }
209230 catch ( Exception ex )
210231 {
211- moveException = ExceptionDispatchInfo . Capture ( ex ) ;
232+ exceptions . Add ( ex ) ;
212233 }
213234 }
214-
215- cts . Cancel ( ) ;
216235 } ) ;
217236
218- t . Start ( ) ;
219-
220- manager . RunAndBlock ( cts . Token ) ;
221-
222- Assert . True ( t . Join ( TimeSpan . FromSeconds ( 10 ) ) , "Thread join timed out ." ) ;
237+ try
238+ {
239+ backgroundThread . Start ( ) ;
240+ manager . RunAndBlock ( ) ;
241+ Assert . True ( backgroundThread . Join ( 60000 ) , "The background task did not complete in 60 seconds ." ) ;
223242
224- Assert . False ( timeout , "The test timed out and was canceled." ) ;
225- string pollString = string . Join ( Environment . NewLine , pollMessages ) ;
226- Assert . True ( exception == null , pollString + Environment . NewLine + exception ? . SourceException ? . ToString ( ) ) ;
227- Assert . True ( moveException == null , "Failed to move function back to original directory: " + moveException ? . SourceException ? . ToString ( ) ) ;
243+ string exceptionString = string . Join ( Environment . NewLine , exceptions . Select ( p => p . ToString ( ) ) ) ;
244+ Assert . True ( exceptions . Count ( ) == 0 , exceptionString ) ;
245+ }
246+ finally
247+ {
248+ // Move the directory back after the host has stopped to prevent
249+ // unnecessary host restarts
250+ if ( Directory . Exists ( newDirectory ) )
251+ {
252+ Directory . Move ( newDirectory , oldDirectory ) ;
253+ }
254+ }
228255 }
229256 }
230257
@@ -694,13 +721,15 @@ private static CloudBlockBlob UpdateOutputName(string prev, string hint, EndToEn
694721 {
695722 string name = hint ;
696723
724+ // As soon as we touch the file, the trigger may reload, so delete any existing blob first.
725+ var blob = fixture . TestOutputContainer . GetBlockBlobReference ( name ) ;
726+ blob . DeleteIfExists ( ) ;
727+
697728 string manifestPath = Path . Combine ( Environment . CurrentDirectory , @"TestScripts\Node\TimerTrigger\function.json" ) ;
698729 string content = File . ReadAllText ( manifestPath ) ;
699730 content = content . Replace ( prev , name ) ;
700731 File . WriteAllText ( manifestPath , content ) ;
701732
702- var blob = fixture . TestOutputContainer . GetBlockBlobReference ( name ) ;
703- blob . DeleteIfExists ( ) ;
704733 return blob ;
705734 }
706735
0 commit comments