~strainanalyser/strainanalyser/trunk

« back to all changes in this revision

Viewing changes to livecoding/tests/test_reloading.py

  • Committer: debianpkg at org
  • Date: 2010-04-26 17:52:41 UTC
  • Revision ID: debianpkg@malc.org.uk-20100426175241-fdvokb921w4o1gdk
Reorganise tree; add livecoding module to aid debugging

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import unittest
 
2
import os, sys, time, math
 
3
import inspect, copy
 
4
import logging
 
5
 
 
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)
 
11
 
 
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)
 
17
 
 
18
 
 
19
import namespace
 
20
import reloader
 
21
 
 
22
class ReloadableScriptDirectoryNoUnitTesting(reloader.ReloadableScriptDirectory):
 
23
    unitTest = False
 
24
 
 
25
class CodeReloadingTestCase(TestCase):
 
26
    def setUp(self):
 
27
        self.codeReloader = None
 
28
        
 
29
        scriptDirPath = GetScriptDirectory()
 
30
        scriptFilePath = os.path.join(scriptDirPath, "fileChange.py")
 
31
 
 
32
        if os.path.exists(scriptFilePath):
 
33
            os.remove(scriptFilePath)
 
34
 
 
35
    def tearDown(self):
 
36
        if self.codeReloader is not None:
 
37
            for dirPath in self.codeReloader.directoriesByPath.keys():
 
38
                self.codeReloader.RemoveDirectory(dirPath)
 
39
 
 
40
    def UpdateBaseClass(self, oldBaseClass, newBaseClass):
 
41
        import gc, types
 
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__)
 
53
 
 
54
    def UpdateGlobalReferences(self, oldBaseClass, newBaseClass):
 
55
        """
 
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.
 
59
 
 
60
        So, just replace all references held in dictionaries.  This will hit
 
61
        """
 
62
        import gc, types
 
63
        for ob1 in gc.get_referrers(oldBaseClass):
 
64
            if type(ob1) is dict:
 
65
                for k, v in ob1.items():
 
66
                    if v is oldBaseClass:
 
67
                        logging.debug("Setting '%s' to '%s' in %d", k, newBaseClass, id(ob1))
 
68
                        ob1[k] = newBaseClass
 
69
 
 
70
 
 
71
class CodeReloadingObstacleTests(CodeReloadingTestCase):
 
72
    """
 
73
    Obstacles to fully working code reloading are surmountable.
 
74
    
 
75
    This test case is intended to demonstrate how these obstacles occur and
 
76
    how they can be addressed.
 
77
    """
 
78
 
 
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")
 
85
 
 
86
        # Replace and wrap the builtin.
 
87
        if mangleCallback:
 
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
 
91
 
 
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)
 
96
 
 
97
            oldOpenBuiltin = __builtins__.open
 
98
            __builtins__.open = intermediateOpen
 
99
            try:
 
100
                replacedScriptFileContents = [ False ]
 
101
                result = self.codeReloader.ReloadScript(oldScriptFile)
 
102
            finally:
 
103
                __builtins__.open = oldOpenBuiltin
 
104
 
 
105
            # Verify that fake script contents were injected as requested.
 
106
            self.failUnless(replacedScriptFileContents[0] is True, "Failed to inject the replacement script file")
 
107
        else:
 
108
            result = self.codeReloader.ReloadScript(oldScriptFile)
 
109
 
 
110
        self.failUnless(result is True, "Failed to reload the script file")
 
111
 
 
112
        newScriptFile = scriptDirectory.FindScript(scriptPath)
 
113
        self.failUnless(newScriptFile is not None, "Failed to find the script file after a reload")
 
114
 
 
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")
 
119
        
 
120
        return newScriptFile
 
121
 
 
122
    def testOverwriteDifferentFileBaseClassReload(self):
 
123
        """
 
124
        Reloading approach: Overwrite old objects on reload.
 
125
        Reloading scope: Different file.
 
126
 
 
127
        This test is intended to demonstrate the problems involved in reloading
 
128
        base classes with regard to existing subclasses.
 
129
 
 
130
        Problems:
 
131
        1) Class references used by subclasses, stored outside of the namespace.
 
132
 
 
133
           i.e. import module
 
134
                BaseClass = module.BaseClass
 
135
 
 
136
                class SubClass(BaseClass):
 
137
                    def __init__(self):
 
138
                        BaseClass.__init__(self)
 
139
 
 
140
           When 'module.BaseClass' is updated to a new version, 'BaseClass'
 
141
           will still refer to the old version.
 
142
           
 
143
           'SubClass' will also have the next problem.
 
144
 
 
145
        2) The class reference held by a subclass.
 
146
 
 
147
           i.e. SubClass.__bases__
 
148
 
 
149
           When 'module.BaseClass' is updated to a new version, 'SubClass.__bases__'
 
150
           will still hold a reference to the old version.
 
151
 
 
152
        """
 
153
        scriptDirPath = GetScriptDirectory()
 
154
        cr = self.codeReloader = reloader.CodeReloader()
 
155
        cr.scriptDirectoryClass = ReloadableScriptDirectoryNoUnitTesting
 
156
 
 
157
        scriptDirectory = cr.AddDirectory("game", scriptDirPath)
 
158
        self.failUnless(scriptDirectory is not None, "Script loading failure")
 
159
 
 
160
        import game
 
161
 
 
162
        oldStyleClass = game.OldStyleBase
 
163
        newStyleClass = game.NewStyleBase
 
164
 
 
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()
 
176
 
 
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()
 
185
 
 
186
        self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py")
 
187
 
 
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()
 
196
 
 
197
        # A) Accessed the base class via namespace, got incompatible post-reload version.
 
198
        # B) Same as A.
 
199
 
 
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()
 
206
 
 
207
        # *) Fail for same reason as the calls to the pre-reload instances.
 
208
 
 
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()
 
217
 
 
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)
 
221
 
 
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()
 
230
 
 
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.
 
235
 
 
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()
 
244
 
 
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()
 
251
 
 
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()
 
260
 
 
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)
 
265
 
 
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()
 
275
 
 
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()
 
284
 
 
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()
 
293
 
 
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()
 
301
 
 
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()
 
310
 
 
311
    def testOverwriteSameFileClassReload(self):
 
312
        """
 
313
        Reloading approach: Overwrite old objects on reload.
 
314
        Reloading scope: Same file.
 
315
        
 
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.
 
319
 
 
320
        4. Reload the script the classes were exported from.
 
321
 
 
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.
 
326
 
 
327
        This verifies that instances linked to old superceded
 
328
        versions of a class, still work.
 
329
        """
 
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")
 
335
        
 
336
        import game
 
337
 
 
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()
 
343
 
 
344
        newStyleBaseClass = game.NewStyleBase
 
345
        newStyleBaseClassInstance = newStyleBaseClass()
 
346
        newStyleClass = game.NewStyle
 
347
        newStyleClassInstance = newStyleClass()
 
348
 
 
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()
 
355
 
 
356
        self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py")
 
357
 
 
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")
 
363
 
 
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()
 
370
 
 
371
        # Make some new instances from the old class references.
 
372
        oldStyleBaseClassInstance = oldStyleBaseClass()
 
373
        oldStyleClassInstance = oldStyleClass()
 
374
        newStyleBaseClassInstance = newStyleBaseClass()
 
375
        newStyleClassInstance = newStyleClass()
 
376
        
 
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()
 
383
        
 
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()
 
389
        
 
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()
 
396
 
 
397
    def testUpdateSameFileReload_ClassFunctionUpdate(self):
 
398
        """
 
399
        Reloading approach: Update old objects on reload.
 
400
        Reloading scope: Same file.
 
401
        
 
402
        1. Get references to the exported classes.
 
403
        2. Get references to functions on those classes.
 
404
 
 
405
        3. Reload the script the classes were exported from.
 
406
 
 
407
        4. Verify the old classes have not been replaced.
 
408
        5. Verify the functions on the classes have been updated.
 
409
 
 
410
        This verifies the existing class functions are updated in place.
 
411
        """
 
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")
 
417
        
 
418
        import game
 
419
 
 
420
        # Obtain references and instances for the classes defined in the script.
 
421
        oldStyleBaseClass = game.OldStyleBase
 
422
        newStyleBaseClass = game.NewStyleBase
 
423
 
 
424
        # Verify that the exposed method can be called on each.
 
425
        oldStyleBaseClassFunc = oldStyleBaseClass.Func_Arguments1
 
426
        newStyleBaseClassFunc = newStyleBaseClass.Func_Arguments1
 
427
 
 
428
        cb = MakeMangleFilenameCallback("inheritanceSuperclasses_FunctionUpdate.py")
 
429
        self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py", mangleCallback=cb)
 
430
 
 
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")
 
435
 
 
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")
 
440
 
 
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")
 
446
 
 
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")
 
450
 
 
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")
 
455
 
 
456
        # Note: Actually, all this cack is updated just by the function having been replaced in-situ.
 
457
 
 
458
    def testUpdateSameFileReload_ClassRemoval(self):
 
459
        """
 
460
        Reloading approach: Update old objects on reload.
 
461
        Reloading scope: Same file.
 
462
        
 
463
        1. Get references to the exported classes.
 
464
        2. Get references to functions on those classes.
 
465
 
 
466
        3. Reload the script the classes were exported from.
 
467
 
 
468
        4. Verify the old classes have not been replaced.
 
469
        5. Verify the functions on the classes have been replaced.
 
470
 
 
471
        This verifies the existing classes are updated in place.
 
472
        """
 
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")
 
478
        
 
479
        import game
 
480
 
 
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
 
486
 
 
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
 
492
 
 
493
        cb = MakeMangleFilenameCallback("inheritanceSuperclasses_ClassRemoval.py")
 
494
        newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "inheritanceSuperclasses.py", mangleCallback=cb)
 
495
 
 
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")
 
502
 
 
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")
 
508
 
 
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")
 
511
 
 
512
    def testUpdateSameFileReload_ClassFunctionAddition(self):
 
513
        pass
 
514
 
 
515
    def testUpdateSameFileReload_ClassFunctionRemoval(self):
 
516
        pass
 
517
 
 
518
    def testUpdateSameFileReload_ClassAddition(self):
 
519
        pass
 
520
 
 
521
    def testUpdateSameFileReload_ClassRemoval(self):
 
522
        pass
 
523
 
 
524
    def testUpdateSameFileReload_FunctionUpdate(self):
 
525
        """
 
526
        Reloading approach: Update old objects on reload.
 
527
        Reloading scope: Same file.
 
528
        
 
529
        1. Get references to the exported functions.
 
530
 
 
531
        2. Reload the script the classes were exported from.
 
532
 
 
533
        3. Verify the old functions have been replaced.
 
534
        4. Verify the functions are callable.
 
535
 
 
536
        This verifies that new contributions are put in place.
 
537
        """
 
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")
 
543
        
 
544
        import game
 
545
 
 
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
 
550
 
 
551
        cb = MakeMangleFilenameCallback("functions_Update.py")
 
552
        newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "functions.py", mangleCallback=cb)
 
553
 
 
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")
 
557
 
 
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")
 
561
        
 
562
        ret = testFunction_PositionalArguments("testarg1", "testarg2", "testarg3")
 
563
        self.failUnless(ret[0] == "testarg1" and ret[1] == ("testarg2", "testarg3"), "Old function failed after reload")
 
564
        
 
565
        ret = testFunction_KeywordArguments(testarg1="testarg1")
 
566
        self.failUnless(ret[0] == "arg1" and ret[1] == {"testarg1":"testarg1"}, "Old function failed after reload")
 
567
 
 
568
        ## Verify that the new functions work.
 
569
        ret = game.TestFunction("testarg1")
 
570
        self.failUnless(ret[0] == "testarg1", "Updated function failed after reload")
 
571
        
 
572
        ret = game.TestFunction_PositionalArguments("testarg1", "testarg2", "testarg3")
 
573
        self.failUnless(ret[0] == "testarg1" and ret[1] == ("testarg2", "testarg3"), "Updated function failed after reload")
 
574
        
 
575
        ret = game.TestFunction_KeywordArguments(testarg1="testarg1")
 
576
        self.failUnless(ret[0] == "newarg1" and ret[1] == {"testarg1":"testarg1"}, "Updated function failed after reload")
 
577
 
 
578
    def testUpdateSameFileReload_ImportAddition(self):
 
579
        """
 
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.
 
582
        """
 
583
        ## PREPARATION:
 
584
 
 
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")
 
590
 
 
591
        import game
 
592
        oldFunction = game.ImportTestClass.TestFunction.im_func
 
593
        self.failUnless("logging" not in oldFunction.func_globals, "Global entry unexpectedly already present")
 
594
        
 
595
        cb = MakeMangleFilenameCallback("import_Update.py")
 
596
        newScriptFile = self.ReloadScriptFile(scriptDirectory, scriptDirPath, "import.py", mangleCallback=cb)
 
597
 
 
598
        ## ACTUAL TESTS:
 
599
 
 
600
        newFunction = game.ImportTestClass.TestFunction.im_func
 
601
        self.failUnless("logging" in newFunction.func_globals, "Global entry unexpectedly already present")
 
602
 
 
603
 
 
604
class CodeReloaderSupportTests(CodeReloadingTestCase):
 
605
    mockedNamespaces = None
 
606
 
 
607
    def tearDown(self):
 
608
        super(self.__class__, self).tearDown()
 
609
 
 
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)
 
616
 
 
617
    def InsertMockNamespaceEntry(self, namespacePath, replacementValue):
 
618
        if self.mockedNamespaces is None:
 
619
            self.mockedNamespaces = {}
 
620
 
 
621
        moduleNamespace, attributeName = namespacePath.rsplit(".", 1)
 
622
        module = __import__(moduleNamespace)
 
623
 
 
624
        # Store the old value.
 
625
        if namespacePath not in self.mockedNamespaces:
 
626
            self.mockedNamespaces[namespacePath] = getattr(module, attributeName)
 
627
 
 
628
        setattr(module, attributeName, replacementValue)
 
629
 
 
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()
 
635
            if ret is None:
 
636
                return None
 
637
 
 
638
            delta = time.time() - startTime
 
639
 
 
640
            newScriptFile = scriptDirectory.FindScript(scriptPath)
 
641
            if newScriptFile is None:
 
642
                continue
 
643
 
 
644
            if oldScriptFile is None and newScriptFile is not None:
 
645
                return delta
 
646
 
 
647
            if oldScriptFile.version < newScriptFile.version:
 
648
                return delta
 
649
 
 
650
    def testDirectoryRegistration(self):
 
651
        """
 
652
        Verify that this function returns a registered handler for a parent
 
653
        directory, if there are any above the given file path.
 
654
        """
 
655
        #self.InsertMockNamespaceEntry("reloader.ReloadableScriptDirectory", DummyClass)
 
656
        #self.failUnless(reloader.ReloadableScriptDirectory is DummyClass, "Failed to mock the script directory class")
 
657
 
 
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 ]
 
664
        handlersByPath = {}
 
665
 
 
666
        baseNamespaceName = "testns"
 
667
 
 
668
        class DummySubClass(DummyClass):
 
669
            def Load(self):
 
670
                return True
 
671
 
 
672
        dummySubClassInstance = DummySubClass()
 
673
        self.failUnless(dummySubClassInstance.Load() is True, "DummySubClass insufficient to fake directory loading")
 
674
        
 
675
        cr = reloader.CodeReloader()        
 
676
        cr.scriptDirectoryClass = DummySubClass
 
677
 
 
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")
 
680
 
 
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
 
685
        
 
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")
 
693
 
 
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")
 
696
 
 
697
    def testAttributeLeaking(self):
 
698
        """
 
699
        This test is intended to exercise the leaked attribute tracking.
 
700
 
 
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:
 
704
        
 
705
        - The removed class is now present in the leaked attribute tracking.
 
706
        - The given leaked attribute is associated with the previous version
 
707
          of the script.
 
708
        - The class is still present in the namespace and is actually leaked.
 
709
        
 
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
 
712
        verified that:
 
713
        
 
714
        - The namespace entry is no longer a leaked attribute.
 
715
        - The class in the namespace is not the original version.
 
716
        """
 
717
    
 
718
        ## PREPARATION:
 
719
    
 
720
        # The name of the attribute we are going to leak as part of this test.
 
721
        leakName = "NewStyleSubclassViaClassReference"
 
722
    
 
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")
 
728
 
 
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")
 
733
 
 
734
        namespacePath = oldScriptFile.namespacePath
 
735
        namespace = scriptDirectory.GetNamespace(namespacePath)
 
736
        leakingValue = getattr(namespace, leakName)
 
737
 
 
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.
 
741
 
 
742
        newScriptFile1 = cr.CreateNewScript(oldScriptFile)
 
743
        self.failUnless(newScriptFile1 is not None, "Failed to create new script file version at attempt one")
 
744
        
 
745
        # Pretend that the programmer deleted a class from the script since the original load.
 
746
        del newScriptFile1.scriptGlobals[leakName]
 
747
 
 
748
        # Replace the old script with the new version.
 
749
        cr.UseNewScript(oldScriptFile, newScriptFile1)
 
750
 
 
751
        ## ACTUAL TESTS:
 
752
 
 
753
        self.failUnless(cr.IsAttributeLeaked(leakName), "Attribute not in leakage registry")
 
754
 
 
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))
 
758
 
 
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")
 
762
 
 
763
        ## PREPARATION:
 
764
 
 
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.
 
768
 
 
769
        newScriptFile2 = cr.CreateNewScript(newScriptFile1)
 
770
        self.failUnless(newScriptFile2 is not None, "Failed to create new script file version at attempt two")
 
771
 
 
772
        # Pretend that the programmer deleted a class from the script since the original load.
 
773
        del newScriptFile2.scriptGlobals[leakName]
 
774
 
 
775
        # Replace the old script with the new version.
 
776
        cr.UseNewScript(newScriptFile1, newScriptFile2)
 
777
 
 
778
        ## ACTUAL TESTS:
 
779
 
 
780
        self.failUnless(cr.IsAttributeLeaked(leakName), "Attribute not in leakage registry")
 
781
 
 
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))
 
785
 
 
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")
 
789
        
 
790
        ## PREPARATION:
 
791
 
 
792
        #  - Attribute was already leaked, and reload comes with an invalid replacement.
 
793
        #    - Reload is rejected.
 
794
 
 
795
        ## ACTUAL TESTS:
 
796
 
 
797
        logging.warn("TODO, implement leakage compatibility case")
 
798
 
 
799
        ## PREPARATION:
 
800
 
 
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.
 
804
 
 
805
        newScriptFile3 = cr.CreateNewScript(newScriptFile2)
 
806
        self.failUnless(newScriptFile3 is not None, "Failed to create new script file version at attempt two")
 
807
 
 
808
        # Replace the old script with the new version.
 
809
        cr.UseNewScript(newScriptFile2, newScriptFile3)
 
810
        
 
811
        newValue = newScriptFile3.scriptGlobals[leakName]
 
812
 
 
813
        ## ACTUAL TESTS:
 
814
 
 
815
        self.failUnless(not cr.IsAttributeLeaked(leakName), "Attribute still in leakage registry")
 
816
 
 
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")
 
821
 
 
822
        # Conclusion: Attribute leaking happens and is rectified.
 
823
 
 
824
    def testFileChangeDetection(self):
 
825
        """
 
826
        This test is intended to verify that file changes are detected and a reload takes place.
 
827
        """
 
828
    
 
829
        ## PREPARATION:
 
830
 
 
831
        scriptDirPath = GetScriptDirectory()
 
832
        scriptFilePath = os.path.join(scriptDirPath, "fileChange.py")
 
833
        script2DirPath = scriptDirPath +"2"
 
834
        
 
835
        self.failUnless(not os.path.exists(scriptFilePath), "Found script when it should have been deleted")
 
836
 
 
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")
 
843
 
 
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)
 
847
 
 
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)
 
851
 
 
852
        oldScriptFile = scriptDirectory.FindScript(scriptFilePath)
 
853
        self.failUnless(oldScriptFile is None, "Found the script file before it was created")
 
854
 
 
855
        open(scriptFilePath, "w").write(open(sourceScriptFilePath, "r").read())
 
856
        self.failUnless(os.path.exists(scriptFilePath), "Failed to create the scratch file")
 
857
 
 
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()):
 
861
            pass
 
862
 
 
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)
 
866
 
 
867
        import game
 
868
        self.failUnless(game.FileChangeFunction.__doc__ == " old version ", "Expected function doc string value not present")
 
869
 
 
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)
 
873
 
 
874
        oldScriptFile = scriptDirectory.FindScript(scriptFilePath)
 
875
        self.failUnless(oldScriptFile is not None, "Did not find a loaded script file")
 
876
 
 
877
        ## BEHAVIOUR TO BE TESTED:
 
878
 
 
879
        # Change the monitored script.
 
880
        open(scriptFilePath, "w").write(open(sourceScriptFilePath, "r").read())
 
881
        
 
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)
 
885
 
 
886
        ## ACTUAL TESTS:
 
887
 
 
888
        newDocString = game.FileChangeFunction.__doc__
 
889
        self.failUnless(newDocString == " new version ", "Updated function doc string value '"+ newDocString +"'")
 
890
 
 
891
    def testScriptUnitTesting(self):
 
892
        """
 
893
        This test is intended to verify that local unit test failure equals code loading failure.
 
894
        
 
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.
 
898
        """
 
899
 
 
900
        ## PREPARATION:
 
901
 
 
902
        scriptDirPath = GetScriptDirectory()
 
903
        self.codeReloader = reloader.CodeReloader()
 
904
 
 
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)
 
909
                
 
910
                # These are the starting words of the lines we want to suppress.
 
911
                self.lineStartsWiths = [
 
912
                    "ScriptDirectory.Load",
 
913
                    "Error",
 
914
                    "Failure",
 
915
                    "Traceback",
 
916
                ]
 
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 = []
 
921
 
 
922
            def filter(self, record):
 
923
                self.lineCount -= 1
 
924
 
 
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))
 
931
                    else:
 
932
                        # Give up on filtering on unexpected input.
 
933
                        self.lineStartsWiths = []
 
934
                    return 0
 
935
 
 
936
                # Othewise, log away.
 
937
                return 1
 
938
 
 
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)
 
943
 
 
944
        ## BEHAVIOUR TO BE TESTED: Forced unit test and therefore operation failure.
 
945
 
 
946
        __builtins__.unitTestFailure = True
 
947
        try:
 
948
            scriptDirectory = self.codeReloader.AddDirectory("game", scriptDirPath)
 
949
        finally:
 
950
            logger.removeFilter(loggingFilter)
 
951
            del __builtins__.unitTestFailure
 
952
 
 
953
        ## ACTUAL TESTS:
 
954
 
 
955
        self.failUnless(scriptDirectory is None, "Unit tests unexpectedly passed")
 
956
 
 
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")
 
963
 
 
964
        ## BEHAVIOUR TO BE TESTED: Unit test passing and successful operation.
 
965
 
 
966
        __builtins__.unitTestFailure = False
 
967
        try:
 
968
            scriptDirectory = self.codeReloader.AddDirectory("game", scriptDirPath)
 
969
        finally:
 
970
            del __builtins__.unitTestFailure
 
971
 
 
972
        ## ACTUAL TESTS:
 
973
 
 
974
        self.failUnless(scriptDirectory is not None, "Unit tests unexpectedly failed")
 
975
 
 
976
 
 
977
class CodeReloadingLimitationTests(TestCase):
 
978
    """
 
979
    There are limitations to how well code reloading can work.
 
980
    
 
981
    This test case is intended to highlight these limitations so that they
 
982
    are known well enough to be worked with.
 
983
    """
 
984
 
 
985
    def testLocalVariableDirectModificationLimitation(self):
 
986
        """
 
987
        Demonstrate that local variables cannot be indirectly modified via locals().
 
988
        """
 
989
        def ModifyLocal():
 
990
            localValue = 1
 
991
            locals()["localValue"] = 2
 
992
            return localValue
 
993
 
 
994
        value = ModifyLocal()
 
995
        self.failUnless(value == 1, "Local variable unexpectedly indirectly modified")
 
996
 
 
997
        # Conclusion: Local variables are an unavoidable problem when code reloading.
 
998
 
 
999
    def testLocalVariableFrameModificationLimitation(self):
 
1000
        """
 
1001
        Demonstrate that local variables cannot be indirectly modified via frame references.
 
1002
        """
 
1003
        expectedValue = 1
 
1004
 
 
1005
        def ModifyLocal():
 
1006
            localValue = expectedValue
 
1007
            yield localValue
 
1008
            yield localValue
 
1009
 
 
1010
        g = ModifyLocal()
 
1011
 
 
1012
        # Verify that the first generated value is the expected value.
 
1013
        v = g.next()
 
1014
        self.failUnless(v == expectedValue, "Initial local variable value %s, expected %d" % (v, expectedValue))
 
1015
 
 
1016
        f_locals = g.gi_frame.f_locals
 
1017
 
 
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
 
1022
 
 
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))
 
1026
 
 
1027
        # Verify that the second generated value is unchanged and still the expected value.
 
1028
        v = g.next()
 
1029
        self.failUnless(v == expectedValue, "Initial local variable value %s, expected %d" % (v, expectedValue))
 
1030
        
 
1031
        # Conclusion: Local variables are an unavoidable problem when code reloading.
 
1032
 
 
1033
    def testImmutableMethodModificationLimitation(self):
 
1034
        """
 
1035
        Demonstrate that methods are static, and cannot be updated in place.
 
1036
        """
 
1037
        class TestClass:
 
1038
            def TestMethod(self):
 
1039
                pass
 
1040
        testInstance = TestClass()
 
1041
 
 
1042
        unboundMethod = TestClass.TestMethod
 
1043
        self.failUnlessRaises(TypeError, lambda: setattr(unboundMethod, "im_class", self.__class__))
 
1044
 
 
1045
        boundMethod = testInstance.TestMethod
 
1046
        self.failUnlessRaises(TypeError, lambda: setattr(boundMethod, "im_class", self.__class__))
 
1047
 
 
1048
        # Conclusion: Existing references to methods are an unavoidable problem when code reloading.
 
1049
 
 
1050
 
 
1051
class DummyClass:
 
1052
    def __init__(self, *args, **kwargs):
 
1053
        pass
 
1054
 
 
1055
    def __call__(self, *args, **kwargs):
 
1056
        pass
 
1057
 
 
1058
    def __getattr__(self, attrName, defaultValue=None):
 
1059
        if attrName.startswith("__"):
 
1060
            return getattr(DummyClass, attrName)
 
1061
 
 
1062
        instance = self.__class__()
 
1063
        instance.attrName = attrName
 
1064
        instance.defaultValue = defaultValue
 
1065
        return instance
 
1066
 
 
1067
 
 
1068
def MakeMangleFilenameCallback(newFileName):
 
1069
    """
 
1070
    Encapsulate substituting a different script name.
 
1071
    """
 
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)
 
1076
 
 
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]
 
1082
    return dirPath
 
1083
 
 
1084
def GetScriptDirectory():
 
1085
    parentDirPath = GetCurrentDirectory()
 
1086
    return os.path.join(os.path.dirname(parentDirPath), "scripts")
 
1087
 
 
1088
if __name__ == "__main__":
 
1089
    logging.basicConfig(level=logging.WARNING)
 
1090
 
 
1091
    unittest.main()