2
import os, sys, time, math
6
if __name__ == "__main__":
7
currentPath = sys.path[0]
8
parentPath = os.path.dirname(currentPath)
9
if parentPath not in sys.path:
10
sys.path.append(parentPath)
12
# Add test information to the logging output.
13
class TestCase(unittest.TestCase):
14
def run(self, *args, **kwargs):
15
logging.debug("%s %s", self._testMethodName, (79 - len(self._testMethodName) - 1) *"-")
16
super(TestCase, self).run(*args, **kwargs)
22
class ReloadableScriptDirectoryNoUnitTesting(reloader.ReloadableScriptDirectory):
25
class CodeReloadingTestCase(TestCase):
27
self.codeReloader = None
29
scriptDirPath = GetScriptDirectory()
30
scriptFilePath = os.path.join(scriptDirPath, "fileChange.py")
32
if os.path.exists(scriptFilePath):
33
os.remove(scriptFilePath)
36
if self.codeReloader is not None:
37
for dirPath in self.codeReloader.directoriesByPath.keys():
38
self.codeReloader.RemoveDirectory(dirPath)
40
def UpdateBaseClass(self, oldBaseClass, newBaseClass):
42
for ob1 in gc.get_referrers(oldBaseClass):
43
# Class '__bases__' references are stored in a tuple.
44
if type(ob1) is tuple:
45
# We need the subclass which uses those base classes.
46
for ob2 in gc.get_referrers(ob1):
47
if type(ob2) in (types.ClassType, types.TypeType):
48
if ob2.__bases__ is ob1:
49
__bases__ = list(ob2.__bases__)
50
idx = __bases__.index(oldBaseClass)
51
__bases__[idx] = newBaseClass
52
ob2.__bases__ = tuple(__bases__)
54
def UpdateGlobalReferences(self, oldBaseClass, newBaseClass):
56
References to the old version of the class might be held in global dictionaries.
57
- Do not worry about replacing references held in local dictionaries, as this
58
is not possible. Those references are held by the relevant frames.
60
So, just replace all references held in dictionaries. This will hit
63
for ob1 in gc.get_referrers(oldBaseClass):
65
for k, v in ob1.items():
67
logging.debug("Setting '%s' to '%s' in %d", k, newBaseClass, id(ob1))
71
class CodeReloadingObstacleTests(CodeReloadingTestCase):
73
Obstacles to fully working code reloading are surmountable.
75
This test case is intended to demonstrate how these obstacles occur and
76
how they can be addressed.
79
def ReloadScriptFile(self, scriptDirectory, scriptDirPath, scriptFileName, mangleCallback=None):
80
# Get a reference to the original script file object.
81
scriptPath = os.path.join(scriptDirPath, scriptFileName)
82
oldScriptFile = scriptDirectory.FindScript(scriptPath)
83
self.failUnless(oldScriptFile is not None, "Failed to find the existing loaded script file version")
84
self.failUnless(isinstance(oldScriptFile, reloader.ReloadableScriptFile), "Obtained non-reloadable script file object")
86
# Replace and wrap the builtin.
88
def intermediateOpen(openFileName, *args, **kwargs):
89
# The flag needs to be in a place where we can modify it from here.
90
replacedScriptFileContents[0] = True
92
replacementFileName = mangleCallback(openFileName)
93
logging.debug("Mangle file interception %s", openFileName)
94
logging.debug("Mangle file substitution %s", replacementFileName)
95
return oldOpenBuiltin(replacementFileName, *args, **kwargs)
97
oldOpenBuiltin = __builtins__.open
98
__builtins__.open = intermediateOpen
100
replacedScriptFileContents = [ False ]
101
result = self.codeReloader.ReloadScript(oldScriptFile)
103
__builtins__.open = oldOpenBuiltin
105
# Verify that fake script contents were injected as requested.
106
self.failUnless(replacedScriptFileContents[0] is True, "Failed to inject the replacement script file")
108
result = self.codeReloader.ReloadScript(oldScriptFile)
110
self.failUnless(result is True, "Failed to reload the script file")
112
newScriptFile = scriptDirectory.FindScript(scriptPath)
113
self.failUnless(newScriptFile is not None, "Failed to find the script file after a reload")
115
if self.codeReloader.mode == reloader.MODE_OVERWRITE:
116
self.failUnless(newScriptFile is not oldScriptFile, "The registered script file is still the old version")
117
elif self.codeReloader.mode == reloader.MODE_UPDATE:
118
self.failUnless(newScriptFile is oldScriptFile, "The registered script file is no longer the old version")
122
def testOverwriteDifferentFileBaseClassReload(self):
124
Reloading approach: Overwrite old objects on reload.
125
Reloading scope: Different file.
127
This test is intended to demonstrate the problems involved in reloading
128
base classes with regard to existing subclasses.
131
1) Class references used by subclasses, stored outside of the namespace.
134
BaseClass = module.BaseClass
136
class SubClass(BaseClass):
138
BaseClass.__init__(self)
140
When 'module.BaseClass' is updated to a new version, 'BaseClass'
141
will still refer to the old version.
143
'SubClass' will also have the next problem.
145
2) The class reference held by a subclass.
147
i.e. SubClass.__bases__
149
When 'module.BaseClass' is updated to a new version, 'SubClass.__bases__'
150
will still hold a reference to the old version.
153
scriptDirPath = GetScriptDirectory()
154
cr = self.codeReloader = reloader.CodeReloader()
155
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
157
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
158
self.failUnless(scriptDirectory is not None, "Script loading failure")
162
oldStyleClass = game.OldStyleBase
163
newStyleClass = game.NewStyleBase
165
## Obtain references and instances for the two classes defined in the script.
166
oldStyleNamespaceClass = game.OldStyleSubclassViaNamespace
167
oldStyleNamespaceClassInstance1 = oldStyleNamespaceClass()
168
oldStyleGlobalReferenceClass = game.OldStyleSubclassViaGlobalReference
169
oldStyleGlobalReferenceClassInstance1 = oldStyleGlobalReferenceClass()
170
newStyleNamespaceClass = game.NewStyleSubclassViaNamespace
171
newStyleNamespaceClassInstance1 = newStyleNamespaceClass()
172
newStyleGlobalReferenceClass = game.NewStyleSubclassViaGlobalReference
173
newStyleGlobalReferenceClassInstance1 = newStyleGlobalReferenceClass()
174
newStyleClassReferenceClass = game.NewStyleSubclassViaClassReference
175
newStyleClassReferenceClassInstance1 = newStyleClassReferenceClass()
177
## Verify that all the functions are callable before the reload.
178
oldStyleNamespaceClassInstance1.Func()
179
oldStyleGlobalReferenceClassInstance1.Func()
180
newStyleNamespaceClassInstance1.Func()
181
newStyleNamespaceClassInstance1.FuncSuper()
182
newStyleGlobalReferenceClassInstance1.Func()
183
newStyleGlobalReferenceClassInstance1.FuncSuper()
184
newStyleClassReferenceClassInstance1.Func()
186
self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py")
188
## Call functions on the instances created pre-reload.
189
self.failUnlessRaises(TypeError, oldStyleNamespaceClassInstance1.Func) # A
190
oldStyleGlobalReferenceClassInstance1.Func()
191
self.failUnlessRaises(TypeError, newStyleNamespaceClassInstance1.Func) # B
192
newStyleNamespaceClassInstance1.FuncSuper()
193
newStyleGlobalReferenceClassInstance1.Func()
194
newStyleGlobalReferenceClassInstance1.FuncSuper()
195
newStyleClassReferenceClassInstance1.Func()
197
# A) Accessed the base class via namespace, got incompatible post-reload version.
200
## Create new post-reload instances of the subclasses.
201
self.failUnlessRaises(TypeError, game.OldStyleSubclassViaNamespace)
202
oldStyleGlobalReferenceClassInstance2 = game.OldStyleSubclassViaGlobalReference()
203
self.failUnlessRaises(TypeError, game.NewStyleSubclassViaNamespace)
204
newStyleGlobalReferenceClassInstance2 = game.NewStyleSubclassViaGlobalReference()
205
newStyleClassReferenceClassInstance2 = game.NewStyleSubclassViaClassReference()
207
# *) Fail for same reason as the calls to the pre-reload instances.
209
## Call functions on the instances created post-reload.
210
# oldStyleNamespaceClassInstance2.Func()
211
oldStyleGlobalReferenceClassInstance2.Func()
212
# newStyleNamespaceClassInstance2.Func()
213
# newStyleNamespaceClassInstance2.FuncSuper()
214
newStyleGlobalReferenceClassInstance2.Func()
215
newStyleGlobalReferenceClassInstance2.FuncSuper()
216
newStyleClassReferenceClassInstance2.Func()
218
## Pre-reload instances get their base class replaced with the new version.
219
self.UpdateBaseClass(oldStyleClass, game.OldStyleBase)
220
self.UpdateBaseClass(newStyleClass, game.NewStyleBase)
222
## Call functions on the instances created pre-reload.
223
oldStyleNamespaceClassInstance1.Func() # A
224
self.failUnlessRaises(TypeError, oldStyleGlobalReferenceClassInstance1.Func) # B
225
newStyleNamespaceClassInstance1.Func() # C
226
newStyleNamespaceClassInstance1.FuncSuper()
227
self.failUnlessRaises(TypeError, newStyleGlobalReferenceClassInstance1.Func) # D
228
newStyleGlobalReferenceClassInstance1.FuncSuper()
229
newStyleClassReferenceClassInstance1.Func()
231
# A) Fixed, due to base class update.
232
# B) The base class is now post-reload, the global reference still pre-reload.
233
# C) Fixed, due to base class update.
234
# D) The base class is now post-reload, the global reference still pre-reload.
236
## Call functions on the instances created post-reload.
237
# oldStyleNamespaceClassInstance2.Func()
238
self.failUnless(TypeError, oldStyleGlobalReferenceClassInstance2.Func)
239
# newStyleNamespaceClassInstance2.Func()
240
# newStyleNamespaceClassInstance2.FuncSuper()
241
self.failUnlessRaises(TypeError, newStyleGlobalReferenceClassInstance2.Func)
242
newStyleGlobalReferenceClassInstance2.FuncSuper()
243
newStyleClassReferenceClassInstance2.Func()
245
## Create new post-reload post-update instances of the subclasses.
246
oldStyleNamespaceClassInstance3 = game.OldStyleSubclassViaNamespace()
247
self.failUnlessRaises(TypeError, game.OldStyleSubclassViaGlobalReference)
248
newStyleNamespaceClassInstance3 = game.NewStyleSubclassViaNamespace()
249
self.failUnlessRaises(TypeError, game.NewStyleSubclassViaGlobalReference)
250
newStyleClassReferenceClassInstance3 = game.NewStyleSubclassViaClassReference()
252
## Call functions on the instances created post-reload post-update.
253
oldStyleNamespaceClassInstance3.Func()
254
#oldStyleGlobalReferenceClassInstance3.Func()
255
newStyleNamespaceClassInstance3.Func()
256
newStyleNamespaceClassInstance3.FuncSuper()
257
#newStyleGlobalReferenceClassInstance3.Func()
258
#newStyleGlobalReferenceClassInstance3.FuncSuper()
259
newStyleClassReferenceClassInstance3.Func()
261
logging.debug("Test updating global references for 'game.OldStyleBase'")
262
self.UpdateGlobalReferences(oldStyleClass, game.OldStyleBase)
263
logging.debug("Test updating global references for 'game.NewStyleBase'")
264
self.UpdateGlobalReferences(newStyleClass, game.NewStyleBase)
266
### All calls on instances created at any point, should now work.
267
## Call functions on the instances created pre-reload.
268
oldStyleNamespaceClassInstance1.Func()
269
oldStyleGlobalReferenceClassInstance1.Func()
270
newStyleNamespaceClassInstance1.Func()
271
newStyleNamespaceClassInstance1.FuncSuper()
272
newStyleGlobalReferenceClassInstance1.Func()
273
newStyleGlobalReferenceClassInstance1.FuncSuper()
274
newStyleClassReferenceClassInstance1.Func()
276
## Call functions on the instances created post-reload.
277
# oldStyleNamespaceClassInstance2.Func()
278
oldStyleGlobalReferenceClassInstance2.Func()
279
# newStyleNamespaceClassInstance2.Func()
280
# newStyleNamespaceClassInstance2.FuncSuper()
281
newStyleGlobalReferenceClassInstance2.Func()
282
newStyleGlobalReferenceClassInstance2.FuncSuper()
283
newStyleClassReferenceClassInstance2.Func()
285
## Call functions on the instances created post-reload post-update.
286
oldStyleNamespaceClassInstance3.Func()
287
#oldStyleGlobalReferenceClassInstance3.Func()
288
newStyleNamespaceClassInstance3.Func()
289
newStyleNamespaceClassInstance3.FuncSuper()
290
#newStyleGlobalReferenceClassInstance3.Func()
291
#newStyleGlobalReferenceClassInstance3.FuncSuper()
292
newStyleClassReferenceClassInstance3.Func()
294
### New instances from the classes should be creatable.
295
## Instantiate the classes.
296
oldStyleNamespaceClassInstance4 = game.OldStyleSubclassViaNamespace()
297
oldStyleGlobalReferenceClassInstance4 = game.OldStyleSubclassViaGlobalReference()
298
newStyleNamespaceClassInstance4 = game.NewStyleSubclassViaNamespace()
299
newStyleGlobalReferenceClassInstance4 = game.NewStyleSubclassViaGlobalReference()
300
newStyleClassReferenceClassInstance4 = game.NewStyleSubclassViaClassReference()
302
## Call functions on the instances.
303
oldStyleNamespaceClassInstance4.Func()
304
oldStyleGlobalReferenceClassInstance4.Func()
305
newStyleNamespaceClassInstance4.Func()
306
newStyleNamespaceClassInstance4.FuncSuper()
307
newStyleGlobalReferenceClassInstance4.Func()
308
newStyleGlobalReferenceClassInstance4.FuncSuper()
309
newStyleClassReferenceClassInstance4.Func()
311
def testOverwriteSameFileClassReload(self):
313
Reloading approach: Overwrite old objects on reload.
314
Reloading scope: Same file.
316
1. Get references to the exported classes.
317
2. Instantiate an instance from each class.
318
3. Call the functions exposed by each instance.
320
4. Reload the script the classes were exported from.
322
5. Verify that the old classes were replaced with new ones.
323
6. Call the functions exposed by each old class instance.
324
7. Instantiate an instance of each new class.
325
8. Call the functions exposed by each new class instance.
327
This verifies that instances linked to old superceded
328
versions of a class, still work.
330
scriptDirPath = GetScriptDirectory()
331
cr = self.codeReloader = reloader.CodeReloader()
332
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
333
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
334
self.failUnless(scriptDirectory is not None, "Script loading failure")
338
# Obtain references and instances for the classes defined in the script.
339
oldStyleBaseClass = game.OldStyleBase
340
oldStyleBaseClassInstance = oldStyleBaseClass()
341
oldStyleClass = game.OldStyle
342
oldStyleClassInstance = oldStyleClass()
344
newStyleBaseClass = game.NewStyleBase
345
newStyleBaseClassInstance = newStyleBaseClass()
346
newStyleClass = game.NewStyle
347
newStyleClassInstance = newStyleClass()
349
# Verify that the exposed method can be called on each.
350
oldStyleBaseClassInstance.Func()
351
oldStyleClassInstance.Func()
352
newStyleBaseClassInstance.Func()
353
newStyleClassInstance.Func()
354
newStyleClassInstance.FuncSuper()
356
self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py")
358
# Verify that the original classes were replaced with new versions.
359
self.failUnless(oldStyleBaseClass is not game.OldStyleBase, "Failed to replace the original 'game.OldStyleBase' class")
360
self.failUnless(oldStyleClass is not game.OldStyle, "Failed to replace the original 'game.OldStyle' class")
361
self.failUnless(newStyleBaseClass is not game.NewStyleBase, "Failed to replace the original 'game.NewStyleBase' class")
362
self.failUnless(newStyleClass is not game.NewStyle, "Failed to replace the original 'game.NewStyle' class")
364
# Verify that the exposed method can be called on the pre-existing instances.
365
oldStyleBaseClassInstance.Func()
366
oldStyleClassInstance.Func()
367
newStyleBaseClassInstance.Func()
368
newStyleClassInstance.Func()
369
newStyleClassInstance.FuncSuper()
371
# Make some new instances from the old class references.
372
oldStyleBaseClassInstance = oldStyleBaseClass()
373
oldStyleClassInstance = oldStyleClass()
374
newStyleBaseClassInstance = newStyleBaseClass()
375
newStyleClassInstance = newStyleClass()
377
# Verify that the exposed method can be called on the new instances.
378
oldStyleBaseClassInstance.Func()
379
oldStyleClassInstance.Func()
380
newStyleBaseClassInstance.Func()
381
newStyleClassInstance.Func()
382
newStyleClassInstance.FuncSuper()
384
# Make some new instances from the new class references.
385
oldStyleBaseClassInstance = game.OldStyleBase()
386
oldStyleClassInstance = game.OldStyle()
387
newStyleBaseClassInstance = game.NewStyleBase()
388
newStyleClassInstance = game.NewStyle()
390
# Verify that the exposed method can be called on the new instances.
391
oldStyleBaseClassInstance.Func()
392
oldStyleClassInstance.Func()
393
newStyleBaseClassInstance.Func()
394
newStyleClassInstance.Func()
395
newStyleClassInstance.FuncSuper()
397
def testUpdateSameFileReload_ClassFunctionUpdate(self):
399
Reloading approach: Update old objects on reload.
400
Reloading scope: Same file.
402
1. Get references to the exported classes.
403
2. Get references to functions on those classes.
405
3. Reload the script the classes were exported from.
407
4. Verify the old classes have not been replaced.
408
5. Verify the functions on the classes have been updated.
410
This verifies the existing class functions are updated in place.
412
scriptDirPath = GetScriptDirectory()
413
cr = self.codeReloader = reloader.CodeReloader(mode=reloader.MODE_UPDATE)
414
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
415
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
416
self.failUnless(scriptDirectory is not None, "Script loading failure")
420
# Obtain references and instances for the classes defined in the script.
421
oldStyleBaseClass = game.OldStyleBase
422
newStyleBaseClass = game.NewStyleBase
424
# Verify that the exposed method can be called on each.
425
oldStyleBaseClassFunc = oldStyleBaseClass.Func_Arguments1
426
newStyleBaseClassFunc = newStyleBaseClass.Func_Arguments1
428
cb = MakeMangleFilenameCallback("inheritanceSuperclasses_FunctionUpdate.py")
429
self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py", mangleCallback=cb)
431
## Verify that the original classes were not replaced.
432
# This is of course because their contents are updated.
433
self.failUnless(oldStyleBaseClass is game.OldStyleBase, "Failed to keep the original 'game.OldStyleBase' class")
434
self.failUnless(newStyleBaseClass is game.NewStyleBase, "Failed to keep the original 'game.NewStyleBase' class")
436
## Verify that the functions on the classes have been updated in place.
437
# All classes should have had their functions replaced.
438
self.failUnless(oldStyleBaseClassFunc.im_func is not oldStyleBaseClass.Func_Arguments1.im_func, "Class function not updated in place")
439
self.failUnless(newStyleBaseClassFunc.im_func is not newStyleBaseClass.Func_Arguments1.im_func, "Class function not updated in place")
441
## Verify that the argument names are updated
442
# Old style classes should work naturally.
443
ret1 = inspect.getargspec(oldStyleBaseClassFunc.im_func)
444
ret2 = inspect.getargspec(oldStyleBaseClass.Func_Arguments1.im_func)
445
self.failUnless(ret1 != ret2, "Function arguments somehow not updated")
447
ret1 = inspect.getargspec(newStyleBaseClassFunc.im_func)
448
ret2 = inspect.getargspec(newStyleBaseClass.Func_Arguments1.im_func)
449
self.failUnless(ret1 != ret2, "Function arguments somehow not updated")
451
ret = inspect.getargspec(oldStyleBaseClass.Func_Arguments2.im_func)
452
self.failUnless(ret[3] == (True,), "Function argument default value not updated")
453
ret = inspect.getargspec(newStyleBaseClass.Func_Arguments2.im_func)
454
self.failUnless(ret[3] == (True,), "Function argument default value not updated")
456
# Note: Actually, all this cack is updated just by the function having been replaced in-situ.
458
def testUpdateSameFileReload_ClassRemoval(self):
460
Reloading approach: Update old objects on reload.
461
Reloading scope: Same file.
463
1. Get references to the exported classes.
464
2. Get references to functions on those classes.
466
3. Reload the script the classes were exported from.
468
4. Verify the old classes have not been replaced.
469
5. Verify the functions on the classes have been replaced.
471
This verifies the existing classes are updated in place.
473
scriptDirPath = GetScriptDirectory()
474
cr = self.codeReloader = reloader.CodeReloader(mode=reloader.MODE_UPDATE)
475
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
476
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
477
self.failUnless(scriptDirectory is not None, "Script loading failure")
481
# Obtain references and instances for the classes defined in the script.
482
oldStyleBaseClass = game.OldStyleBase
483
oldStyleClass = game.OldStyle
484
newStyleBaseClass = game.NewStyleBase
485
newStyleClass = game.NewStyle
487
# Verify that the exposed method can be called on each.
488
oldStyleBaseClassFunc = oldStyleBaseClass.Func
489
oldStyleClassFunc = oldStyleClass.Func
490
newStyleBaseClassFunc = newStyleBaseClass.Func
491
newStyleClassFunc = newStyleClass.Func
493
cb = MakeMangleFilenameCallback("inheritanceSuperclasses_ClassRemoval.py")
494
newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py", mangleCallback=cb)
496
## Verify that the original classes were not replaced.
497
# This is of course because their contents are updated.
498
self.failUnless(oldStyleBaseClass is game.OldStyleBase, "Failed to keep the original 'game.OldStyleBase' class")
499
self.failUnless(oldStyleClass is game.OldStyle, "Failed to keep the original 'game.OldStyle' class")
500
self.failUnless(newStyleBaseClass is game.NewStyleBase, "Failed to keep the original 'game.NewStyleBase' class")
501
self.failUnless(newStyleClass is game.NewStyle, "Failed to keep the original 'game.NewStyle' class")
503
## Verify that the functions on the classes have been updated in place.
504
# All classes which still exist in the updated script should have had their functions replaced.
505
self.failUnless(oldStyleBaseClassFunc.im_func is not oldStyleBaseClass.Func.im_func, "Class function not updated in place")
506
self.failUnless(newStyleBaseClassFunc.im_func is not newStyleBaseClass.Func.im_func, "Class function not updated in place")
507
self.failUnless(newStyleClassFunc.im_func is not newStyleClass.Func.im_func, "Class function not updated in place")
509
# All classes which have been leaked should remain unchanged.
510
self.failUnless(oldStyleClassFunc.im_func is oldStyleClass.Func.im_func, "Class function not updated in place")
512
def testUpdateSameFileReload_ClassFunctionAddition(self):
515
def testUpdateSameFileReload_ClassFunctionRemoval(self):
518
def testUpdateSameFileReload_ClassAddition(self):
521
def testUpdateSameFileReload_ClassRemoval(self):
524
def testUpdateSameFileReload_FunctionUpdate(self):
526
Reloading approach: Update old objects on reload.
527
Reloading scope: Same file.
529
1. Get references to the exported functions.
531
2. Reload the script the classes were exported from.
533
3. Verify the old functions have been replaced.
534
4. Verify the functions are callable.
536
This verifies that new contributions are put in place.
538
scriptDirPath = GetScriptDirectory()
539
cr = self.codeReloader = reloader.CodeReloader(mode=reloader.MODE_UPDATE)
540
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
541
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
542
self.failUnless(scriptDirectory is not None, "Script loading failure")
546
# Obtain references and instances for the classes defined in the script.
547
testFunction = game.TestFunction
548
testFunction_PositionalArguments = game.TestFunction_PositionalArguments
549
testFunction_KeywordArguments = game.TestFunction_KeywordArguments
551
cb = MakeMangleFilenameCallback("functions_Update.py")
552
newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "functions.py", mangleCallback=cb)
554
self.failUnless(testFunction is not game.TestFunction, "Function not updated by reload")
555
self.failUnless(testFunction_PositionalArguments is not game.TestFunction_PositionalArguments, "Function not updated by reload")
556
self.failUnless(testFunction_KeywordArguments is not game.TestFunction_KeywordArguments, "Function not updated by reload")
558
## Verify that the old functions still work, in case things hold onto references past reloads.
559
ret = testFunction("testarg1")
560
self.failUnless(ret[0] == "testarg1", "Old function failed after reload")
562
ret = testFunction_PositionalArguments("testarg1", "testarg2", "testarg3")
563
self.failUnless(ret[0] == "testarg1" and ret[1] == ("testarg2", "testarg3"), "Old function failed after reload")
565
ret = testFunction_KeywordArguments(testarg1="testarg1")
566
self.failUnless(ret[0] == "arg1" and ret[1] == {"testarg1":"testarg1"}, "Old function failed after reload")
568
## Verify that the new functions work.
569
ret = game.TestFunction("testarg1")
570
self.failUnless(ret[0] == "testarg1", "Updated function failed after reload")
572
ret = game.TestFunction_PositionalArguments("testarg1", "testarg2", "testarg3")
573
self.failUnless(ret[0] == "testarg1" and ret[1] == ("testarg2", "testarg3"), "Updated function failed after reload")
575
ret = game.TestFunction_KeywordArguments(testarg1="testarg1")
576
self.failUnless(ret[0] == "newarg1" and ret[1] == {"testarg1":"testarg1"}, "Updated function failed after reload")
578
def testUpdateSameFileReload_ImportAddition(self):
580
This test is intended to verify that when a changed file adds an import
581
statement, the imported object is present when it is reloaded.
585
scriptDirPath = GetScriptDirectory()
586
cr = self.codeReloader = reloader.CodeReloader(mode=reloader.MODE_UPDATE)
587
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
588
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
589
self.failUnless(scriptDirectory is not None, "Script loading failure")
592
oldFunction = game.ImportTestClass.TestFunction.im_func
593
self.failUnless("logging" not in oldFunction.func_globals, "Global entry unexpectedly already present")
595
cb = MakeMangleFilenameCallback("import_Update.py")
596
newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "import.py", mangleCallback=cb)
600
newFunction = game.ImportTestClass.TestFunction.im_func
601
self.failUnless("logging" in newFunction.func_globals, "Global entry unexpectedly already present")
604
class CodeReloaderSupportTests(CodeReloadingTestCase):
605
mockedNamespaces = None
608
super(self.__class__, self).tearDown()
610
# Restore all the mocked namespace entries.
611
if self.mockedNamespaces is not None:
612
for namespacePath, replacedValue in self.mockedNamespaces.iteritems():
613
moduleNamespace, attributeName = namespacePath.rsplit(".", 1)
614
module = __import__(moduleNamespace)
615
setattr(module, attributeName, replacedValue)
617
def InsertMockNamespaceEntry(self, namespacePath, replacementValue):
618
if self.mockedNamespaces is None:
619
self.mockedNamespaces = {}
621
moduleNamespace, attributeName = namespacePath.rsplit(".", 1)
622
module = __import__(moduleNamespace)
624
# Store the old value.
625
if namespacePath not in self.mockedNamespaces:
626
self.mockedNamespaces[namespacePath] = getattr(module, attributeName)
628
setattr(module, attributeName, replacementValue)
630
def WaitForScriptFileChange(self, cr, scriptDirectory, scriptPath, oldScriptFile, maxDelay=10.0):
631
startTime = time.time()
632
delta = time.time() - startTime
633
while delta < maxDelay:
634
ret = cr.internalFileMonitor.WaitForNextMonitoringCheck()
638
delta = time.time() - startTime
640
newScriptFile = scriptDirectory.FindScript(scriptPath)
641
if newScriptFile is None:
644
if oldScriptFile is None and newScriptFile is not None:
647
if oldScriptFile.version < newScriptFile.version:
650
def testDirectoryRegistration(self):
652
Verify that this function returns a registered handler for a parent
653
directory, if there are any above the given file path.
655
#self.InsertMockNamespaceEntry("reloader.ReloadableScriptDirectory", DummyClass)
656
#self.failUnless(reloader.ReloadableScriptDirectory is DummyClass, "Failed to mock the script directory class")
658
currentDirPath = GetCurrentDirectory()
659
# Add several directories to ensure correct results are returned.
660
scriptDirPath1 = os.path.join(currentDirPath, "scripts1")
661
scriptDirPath2 = os.path.join(currentDirPath, "scripts2")
662
scriptDirPath3 = os.path.join(currentDirPath, "scripts3")
663
scriptDirPaths = [ scriptDirPath1, scriptDirPath2, scriptDirPath3 ]
666
baseNamespaceName = "testns"
668
class DummySubClass(DummyClass):
672
dummySubClassInstance = DummySubClass()
673
self.failUnless(dummySubClassInstance.Load() is True, "DummySubClass insufficient to fake directory loading")
675
cr = reloader.CodeReloader()
676
cr.scriptDirectoryClass = DummySubClass
678
# Test that a bad path will not find a handler when there are no handlers.
679
self.failUnless(cr.FindDirectory("unregistered path") is None, "Got a script directory handler for an unregistered path")
681
for scriptDirPath in scriptDirPaths:
682
handler = cr.AddDirectory(baseNamespaceName, scriptDirPath)
683
self.failUnless(handler is not None, "Script loading failure")
684
handlersByPath[scriptDirPath] = handler
686
# Test that a given valid registered script path gives the known handler for that path.
687
while len(scriptDirPaths):
688
scriptDirPath = scriptDirPaths.pop()
689
fictionalScriptPath = os.path.join(scriptDirPath, "nonExistentScript.py")
690
scriptDirectory = cr.FindDirectory(fictionalScriptPath)
691
self.failUnless(scriptDirectory, "Got no script directory handler instance")
692
self.failUnless(scriptDirectory is handlersByPath[scriptDirPath], "Got a different script directory handler instance")
694
# Test that a bad path will not find a handler when there are valid ones for other paths.
695
self.failUnless(cr.FindDirectory("unregistered path") is None, "Got a script directory handler for an unregistered path")
697
def testAttributeLeaking(self):
699
This test is intended to exercise the leaked attribute tracking.
701
First, a script is loaded exporting a class in a namespace. Next the
702
script is copied and the class is removed from it. The modified copy
703
is then reloaded in place of the original. It is verified that:
705
- The removed class is now present in the leaked attribute tracking.
706
- The given leaked attribute is associated with the previous version
708
- The class is still present in the namespace and is actually leaked.
710
Lastly, a final copy of the script is made, with the class back in
711
place. This is then reloaded in place of the first copy. It is
714
- The namespace entry is no longer a leaked attribute.
715
- The class in the namespace is not the original version.
720
# The name of the attribute we are going to leak as part of this test.
721
leakName = "NewStyleSubclassViaClassReference"
723
scriptDirPath = GetScriptDirectory()
724
cr = self.codeReloader = reloader.CodeReloader()
725
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
726
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
727
self.failUnless(scriptDirectory is not None, "Script loading failure")
729
# Locate the script file object for the 'inheritanceSubclasses.py' file.
730
scriptPath = os.path.join(scriptDirPath, "inheritanceSubclasses.py")
731
oldScriptFile = scriptDirectory.FindScript(scriptPath)
732
self.failUnless(oldScriptFile is not None, "Failed to find initial script file")
734
namespacePath = oldScriptFile.namespacePath
735
namespace = scriptDirectory.GetNamespace(namespacePath)
736
leakingValue = getattr(namespace, leakName)
738
# - Attribute is removed from a new version of the script file.
739
# - Attribute appears in the leaked attributes dictionary of the new script file.
740
# - Attribute is still present in the namespace.
742
newScriptFile1 = cr.CreateNewScript(oldScriptFile)
743
self.failUnless(newScriptFile1 is not None, "Failed to create new script file version at attempt one")
745
# Pretend that the programmer deleted a class from the script since the original load.
746
del newScriptFile1.scriptGlobals[leakName]
748
# Replace the old script with the new version.
749
cr.UseNewScript(oldScriptFile, newScriptFile1)
753
self.failUnless(cr.IsAttributeLeaked(leakName), "Attribute not in leakage registry")
755
# Ensure that the leakage is recorded as coming from the original script.
756
leakedInVersion = cr.GetLeakedAttributeVersion(leakName)
757
self.failUnless(leakedInVersion == oldScriptFile.version, "Attribute was leaked in %d, should have been leaked in %d" % (leakedInVersion, oldScriptFile.version))
759
# Ensure that the leakage is left in the module.
760
self.failUnless(hasattr(namespace, leakName), "Leaked attribute no longer present")
761
self.failUnless(getattr(namespace, leakName) is leakingValue, "Leaked value differs from original value")
765
# - Attribute was already leaked, and reload comes with no replacement.
766
# - New script file has leak entry propagated from old script file.
767
# - Attribute is still present in the namespace.
769
newScriptFile2 = cr.CreateNewScript(newScriptFile1)
770
self.failUnless(newScriptFile2 is not None, "Failed to create new script file version at attempt two")
772
# Pretend that the programmer deleted a class from the script since the original load.
773
del newScriptFile2.scriptGlobals[leakName]
775
# Replace the old script with the new version.
776
cr.UseNewScript(newScriptFile1, newScriptFile2)
780
self.failUnless(cr.IsAttributeLeaked(leakName), "Attribute not in leakage registry")
782
# Ensure that the leakage is recorded as coming from the original script.
783
leakedInVersion = cr.GetLeakedAttributeVersion(leakName)
784
self.failUnless(leakedInVersion == oldScriptFile.version, "Attribute was leaked in %d, should have been leaked in %d" % (leakedInVersion, oldScriptFile.version))
786
# Ensure that the leakage is left in the module.
787
self.failUnless(hasattr(namespace, leakName), "Leaked attribute no longer present")
788
self.failUnless(getattr(namespace, leakName) is leakingValue, "Leaked value differs from original value")
792
# - Attribute was already leaked, and reload comes with an invalid replacement.
793
# - Reload is rejected.
797
logging.warn("TODO, implement leakage compatibility case")
801
# - Attribute was already leaked, and reload comes with a valid replacement.
802
# - New script file lacks leak entry for attribute.
803
# - Attribute in namespace is value from new script file.
805
newScriptFile3 = cr.CreateNewScript(newScriptFile2)
806
self.failUnless(newScriptFile3 is not None, "Failed to create new script file version at attempt two")
808
# Replace the old script with the new version.
809
cr.UseNewScript(newScriptFile2, newScriptFile3)
811
newValue = newScriptFile3.scriptGlobals[leakName]
815
self.failUnless(not cr.IsAttributeLeaked(leakName), "Attribute still in leakage registry")
817
# Ensure that the leakage is left in the module.
818
self.failUnless(hasattr(namespace, leakName), "Leaking attribute no longer present in the namespace")
819
self.failUnless(getattr(namespace, leakName) is not leakingValue, "Leaked value is still contributed to the namespace")
820
self.failUnless(getattr(namespace, leakName) is newValue, "New value is not contributed to the namespace")
822
# Conclusion: Attribute leaking happens and is rectified.
824
def testFileChangeDetection(self):
826
This test is intended to verify that file changes are detected and a reload takes place.
831
scriptDirPath = GetScriptDirectory()
832
scriptFilePath = os.path.join(scriptDirPath, "fileChange.py")
833
script2DirPath = scriptDirPath +"2"
835
self.failUnless(not os.path.exists(scriptFilePath), "Found script when it should have been deleted")
837
# Start up the code reloader.
838
# Lower the file change check frequency, to prevent unnecessary unit test stalling.
839
cr = self.codeReloader = reloader.CodeReloader(monitorFileChanges=True, fileChangeCheckDelay=0.05)
840
cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
841
scriptDirectory = cr.AddDirectory("game", scriptDirPath)
842
self.failUnless(scriptDirectory is not None, "Script loading failure")
844
# Identify the initial script to be loaded.
845
sourceScriptFilePath = os.path.join(script2DirPath, "fileChange_Before.py")
846
self.failUnless(os.path.exists(sourceScriptFilePath), "Failed to locate '%s' script" % sourceScriptFilePath)
848
# Wait for the monitoring to kick in.
849
ret = cr.internalFileMonitor.WaitForNextMonitoringCheck(maxDelay=10.0)
850
self.failUnless(ret is not None, "File change not detected in a timely fashion (%s)" % ret)
852
oldScriptFile = scriptDirectory.FindScript(scriptFilePath)
853
self.failUnless(oldScriptFile is None, "Found the script file before it was created")
855
open(scriptFilePath, "w").write(open(sourceScriptFilePath, "r").read())
856
self.failUnless(os.path.exists(scriptFilePath), "Failed to create the scratch file")
858
# Need to wait for the next second. mtime is only accurate to the nearest second on *nix.
859
mtime = os.stat(scriptFilePath).st_mtime
860
while mtime == math.floor(time.time()):
863
# Wait for the file creation to be detected.
864
ret = self.WaitForScriptFileChange(cr, scriptDirectory, scriptFilePath, oldScriptFile)
865
self.failUnless(ret is not None, "File change not detected in a timely fashion (%s)" % ret)
868
self.failUnless(game.FileChangeFunction.__doc__ == " old version ", "Expected function doc string value not present")
870
# Replace the initially loaded script contents via file operations.
871
sourceScriptFilePath = os.path.join(script2DirPath, "fileChange_After.py")
872
self.failUnless(os.path.exists(sourceScriptFilePath), "Failed to locate '%s' script" % sourceScriptFilePath)
874
oldScriptFile = scriptDirectory.FindScript(scriptFilePath)
875
self.failUnless(oldScriptFile is not None, "Did not find a loaded script file")
877
## BEHAVIOUR TO BE TESTED:
879
# Change the monitored script.
880
open(scriptFilePath, "w").write(open(sourceScriptFilePath, "r").read())
882
# Wait for the next file change to be detected.
883
ret = self.WaitForScriptFileChange(cr, scriptDirectory, scriptFilePath, oldScriptFile)
884
self.failUnless(ret is not None, "File change not detected in a timely fashion (%s)" % ret)
888
newDocString = game.FileChangeFunction.__doc__
889
self.failUnless(newDocString == " new version ", "Updated function doc string value '"+ newDocString +"'")
891
def testScriptUnitTesting(self):
893
This test is intended to verify that local unit test failure equals code loading failure.
895
First, the act of adding a directory containing a failing '*_unittest.py' file is
896
tested. The operation fails. Next, the act of adding the same directory without
897
the unit test failing is tested. This now succeeds.
902
scriptDirPath = GetScriptDirectory()
903
self.codeReloader = reloader.CodeReloader()
905
# We do not want to log the unit test failure we are causing.
906
class NamespaceUnitTestFilter(logging.Filter):
907
def __init__(self, *args, **kwargs):
908
logging.Filter.__init__(self, *args, **kwargs)
910
# These are the starting words of the lines we want to suppress.
911
self.lineStartsWiths = [
912
"ScriptDirectory.Load",
917
# How many lines we expect to have suppressed, for verification purposes.
918
self.lineCount = len(self.lineStartsWiths)
919
# Keep the suppressed lines around in case the filtering hits unexpected lines.
920
self.filteredLines = []
922
def filter(self, record):
925
# If there are lines left to filter, check them.
926
if len(self.lineStartsWiths):
927
txt = self.lineStartsWiths[0]
928
if record.msg.startswith(txt):
929
del self.lineStartsWiths[0]
930
self.filteredLines.append((record.msg, record.args))
932
# Give up on filtering on unexpected input.
933
self.lineStartsWiths = []
936
# Othewise, log away.
939
logger = logging.getLogger()
940
loggingFilter = NamespaceUnitTestFilter("testScriptUnitTesting - logging Filter")
941
# Make sure we remove this, otherwise the logging will leak..
942
logger.addFilter(loggingFilter)
944
## BEHAVIOUR TO BE TESTED: Forced unit test and therefore operation failure.
946
__builtins__.unitTestFailure = True
948
scriptDirectory = self.codeReloader.AddDirectory("game", scriptDirPath)
950
logger.removeFilter(loggingFilter)
951
del __builtins__.unitTestFailure
955
self.failUnless(scriptDirectory is None, "Unit tests unexpectedly passed")
957
# If something went wrong with the filtered logging, log out the suppressed lines.
958
if loggingFilter.lineCount != 0:
959
for msg, args in loggingFilter.filteredLines:
960
logging.error(msg, *args)
961
# Fail unless the filtered logging met expectations.
962
self.failUnless(loggingFilter.lineCount == 0, "Filtered too many lines to cover the unit test failure")
964
## BEHAVIOUR TO BE TESTED: Unit test passing and successful operation.
966
__builtins__.unitTestFailure = False
968
scriptDirectory = self.codeReloader.AddDirectory("game", scriptDirPath)
970
del __builtins__.unitTestFailure
974
self.failUnless(scriptDirectory is not None, "Unit tests unexpectedly failed")
977
class CodeReloadingLimitationTests(TestCase):
979
There are limitations to how well code reloading can work.
981
This test case is intended to highlight these limitations so that they
982
are known well enough to be worked with.
985
def testLocalVariableDirectModificationLimitation(self):
987
Demonstrate that local variables cannot be indirectly modified via locals().
991
locals()["localValue"] = 2
994
value = ModifyLocal()
995
self.failUnless(value == 1, "Local variable unexpectedly indirectly modified")
997
# Conclusion: Local variables are an unavoidable problem when code reloading.
999
def testLocalVariableFrameModificationLimitation(self):
1001
Demonstrate that local variables cannot be indirectly modified via frame references.
1006
localValue = expectedValue
1012
# Verify that the first generated value is the expected value.
1014
self.failUnless(v == expectedValue, "Initial local variable value %s, expected %d" % (v, expectedValue))
1016
f_locals = g.gi_frame.f_locals
1018
# Verify that the frame local value is the expected value.
1019
v = f_locals["localValue"]
1020
self.failUnless(v == expectedValue, "Indirectly referenced local variable value %s, expected %d" % (v, expectedValue))
1021
f_locals["localValue"] = 2
1023
# Verify that the frame local value pretended to change.
1024
v = f_locals["localValue"]
1025
self.failUnless(v == 2, "Indirectly referenced local variable value %s, expected %d" % (v, 2))
1027
# Verify that the second generated value is unchanged and still the expected value.
1029
self.failUnless(v == expectedValue, "Initial local variable value %s, expected %d" % (v, expectedValue))
1031
# Conclusion: Local variables are an unavoidable problem when code reloading.
1033
def testImmutableMethodModificationLimitation(self):
1035
Demonstrate that methods are static, and cannot be updated in place.
1038
def TestMethod(self):
1040
testInstance = TestClass()
1042
unboundMethod = TestClass.TestMethod
1043
self.failUnlessRaises(TypeError, lambda: setattr(unboundMethod, "im_class", self.__class__))
1045
boundMethod = testInstance.TestMethod
1046
self.failUnlessRaises(TypeError, lambda: setattr(boundMethod, "im_class", self.__class__))
1048
# Conclusion: Existing references to methods are an unavoidable problem when code reloading.
1052
def __init__(self, *args, **kwargs):
1055
def __call__(self, *args, **kwargs):
1058
def __getattr__(self, attrName, defaultValue=None):
1059
if attrName.startswith("__"):
1060
return getattr(DummyClass, attrName)
1062
instance = self.__class__()
1063
instance.attrName = attrName
1064
instance.defaultValue = defaultValue
1068
def MakeMangleFilenameCallback(newFileName):
1070
Encapsulate substituting a different script name.
1072
def MangleFilenameCallback(scriptPath, newFileName):
1073
dirPath = os.path.dirname(scriptPath).replace("scripts", "scripts2")
1074
return os.path.join(dirPath, newFileName)
1075
return lambda scriptPath: MangleFilenameCallback(scriptPath, newFileName)
1077
def GetCurrentDirectory():
1078
# There's probably a better way of doing this.
1079
dirPath = os.path.dirname(__file__)
1080
if not len(dirPath):
1081
dirPath = sys.path[0]
1084
def GetScriptDirectory():
1085
parentDirPath = GetCurrentDirectory()
1086
return os.path.join(os.path.dirname(parentDirPath), "scripts")
1088
if __name__ == "__main__":
1089
logging.basicConfig(level=logging.WARNING)