Friday, May 20, 2011

Checking overrides when upgrading

Here's a question that came up in the vwnc mailing list. Suppose that you have an application which has overrides of a number of system methods. I'm also going to start referring to overrides as redefines here, because I never liked the ambiguity between an override in a subclass and the replacement of a definition that VisualWorks uses the term for. So I'll try calling them redefines here, even though it makes for a lot of backspacing.

Anyway, let's say that this was built in VisualWorks 7.6, but now you're upgrading to 7.8, and you want to check which of these methods no longer apply. That can be complicated in general, but a basic first check is if the method we were redefining changed between VisualWorks 7.6 and 7.8. That's a bit tedious to check manually, but we can script it.

Of course, this depends on how we've organized things. The original question came up in the context of having grouped redefinitions by the package that contained the thing being redefined. So, if we redefined a method in "Assets", we'd create a package "Assets patches" and put the redefinition there.

So here's an example of a workspace script for finding these. It assumes that we've published the old version of the base into our local database, and that we've loaded our code into a new image.

     mySession := StoreLoginFactory currentStoreSession.
     versionToUpgradeFrom := '7.7 '.
     patchPackages := Store.Registry allPackages select: [:each |
          each name like: '% patches'].

So, first we need to get a StoreGlorp session which we'll use to do our queries, and we define a variable for how we'll find the old versions. We'll also need the list of patch packages that we have in the image. I used the #like: method to do the matching, which does it in SQL style, rather than the more traditional matches:, or even regular expressions, but they'd all work.

     patchPackages do: [:eachPatchPackage | 
         basePackageName := eachPatchPackage name readStream upTo: Character space.
         currentOverriddenPackage := Store.Registry packageNamed: basePackageName.

   
Now we start a loop over each of our patch packages. The first thing we need to know is what the basic package is, which we do based on the simple naming convention described above.

              baseQuery := Query readOneOf: StorePackage where: [:each | 
                  (each name = basePackageName & 
                          (each version like: '%', versionToUpgradeFrom, '%')].
              baseQuery orderBy: [:each | each timestamp descending].
              baseVersion := mySession execute: baseQuery.


Now we do a query to find the appropriate old version. So this involves a Glorp query to find packages by that name, whose version string matches the variable we set at the beginning. If there are multiples, we take only the latest, by sorting them in descending order and just taking the first one. I'm dealing with the Cincom internal repository, so there will be lots and lots of versions of base code. In a project repository there are probably only a few, making it fairly simple to find this.

Once we've done that, we'll loop over the methods in the patch package, and get the three different versions of the method: ours, the new base image version, and the old base image version. These will be three different kinds of objects, and the APIs for manipulating them and getting them are, well, let's just say not as polymorphic as they might be. Our method will be a CompiledMethod. The new version in the image will be an OverriddenMethod. And the one we read from the database will be a StoreMethodInPackage, mapped to the database. Getting that one is a bit fussy, in that we need to make sure we're asking for the class by name, and the name should be exactly the way it will be in the database.

         eachPatchPackage methods do: [:eachMethodDescription |
                  imageMethod := eachMethodDescription method.
                 oldOverridden := baseVersion 

                       method: imageMethod selector 
                       forClassNamed: imageMethod mclass instanceBehavior
                      absoluteName meta: imageMethod mclass isMeta.
                 newOverridden := Override 

                       selector: imageMethod selector 
                       class: imageMethod mclass 
                       in: currentOverriddenPackage.

Finally, we get the source code for the old and the new versions and check them. If the new base version doesn't exist, then that method was either deleted or moved to another package, and we definitely need to think about our redefinition. And if the source code is different, we also want to think about it. Then there's the question of what to do if there's something to think about. We could easily write that to the file or to the Transcript, but in this case what I've done is open a simple text comparison window on the two. That could get ugly if there are a large number, but is nice to work with for just a few.

                  (newOverridden isNil 
                       or: [oldOverridden sourceCode ~= newOverridden sourceCode]) 
                  ifTrue: [
                         | view |
                         view := SideBySideTextComparisonView new 

                               leftText: oldOverridden sourceCode 
                               rightText: newOverridden sourceCode.
                         ScheduledWindow new component: view; openWithExtent: 800@600]]].


This would need to be tweaked for particular environments, but seems like it might be a good start towards making that sort of migration a bit easier. And if I can figure out how to get the syntax highlighting on this blog working, it might get prettier to read here.