Tuesday, October 09, 2007

Installing with or without administrator privileges

To create the installer for Task Coach on Windows, I use the excellent Innosetup tool. I upgraded to the latest version of Innosetup a while ago but didn't notice immediately that the installer created by this new version of Innosetup required the user to have administrator privileges. Because Task Coach is aimed at ordinary users, that is not acceptable.

It took me some time to find out how to have the installer work for both users with and without administrator privileges. I'm recording the solution here so that other developers may benefit from it.

In the registry section of the Innosetup script I included two versions of the lines that associate the ".tsk" extension with Task Coach. The first four lines (*) are used if the user has administrator privileges (Check: IsAdminLoggedOn), but the last four lines are used if the user has no administrator rights (Check: not IsAdminLoggedOn).

(*) I had to split the lines to prevent them from being clipped. The line continuations are indented.


[Registry]
Root: HKCR; Subkey: ".tsk"; ValueType: string; ValueName: "";
ValueData: "TaskCoach"; Flags: uninsdeletevalue;
Check: IsAdminLoggedOn
Root: HKCR; Subkey: "TaskCoach"; ValueType: string; ValueName: "";
ValueData: "Task Coach File"; Flags: uninsdeletekey;
Check: IsAdminLoggedOn
Root: HKCR; Subkey: "TaskCoach\DefaultIcon"; ValueType: string;
ValueName: ""; ValueData: "{app}\TaskCoach.EXE,0";
Check: IsAdminLoggedOn
Root: HKCR; Subkey: "TaskCoach\shell\open\command";
ValueType: string; ValueName: "";
ValueData: """{app}\TaskCoach.EXE"" ""%%1""";
Check: IsAdminLoggedOn
Root: HKCU; Subkey: "Software\Classes\.tsk"; ValueType: string;
ValueName: ""; ValueData: "TaskCoachFile";
Flags: uninsdeletevalue; Check: not IsAdminLoggedOn
Root: HKCU; Subkey: "Software\Classes\TaskCoachFile";
ValueType: string; ValueName: ""; ValueData: "Task Coach File";
Flags: uninsdeletekey; Check: not IsAdminLoggedOn
Root: HKCU; Subkey: "Software\Classes\TaskCoachFile\DefaultIcon";
ValueType: string; ValueName: "";
ValueData: "{app}\TaskCoach.EXE,0"; Check: not IsAdminLoggedOn
Root: HKCU; Subkey: "Software\Classes\TaskCoachFile\shell\open\command";
ValueType: string; ValueName: "";
ValueData: """{app}\TaskCoach.EXE"" ""%%1""";
Check: not IsAdminLoggedOn

Sunday, August 26, 2007

Testing translations

A recent bug in Task Coach was caused by one of the translations being incorrect. So, I decided it was time to unittest the translations. For each translated string I wanted to check that certain conditions hold. For example, if the original string has a formatting operator (e.g. '%d' for digits) the translated string should contain the same formatting operator. These tests are relatively simple:

for formatter in '%s', '%d', '%.2f':
self.assertEqual(self.englishString.count(formatter),
self.translatedString.count(formatter))

The challenge is how to create one unittest for each (language, string)-pair. This is not a good solution:

def testMatchingFormatting(self):
for language in getLanguages():
for english, translated in language.dictionary():
...

because this unittest stops as soon as one translation is incorrect.

My first thought was that I could use decorators to unfold the loop, but after a few feeble attempts I decided I am not smart enough to wrap my head around decorators. After some more experimenting I ended up with the code below. I put the loop outside the test class and explicitly create a new TestCase class for each (language, string)-pair. This generates a lot of unittests (over 7600 for the current version of Task Coach), but they run in less than 0.5 seconds, so that's a small price to pay for increased test coverage.


import test, i18n, meta, string

class TranslationIntegrityTests(object):
''' Unittests for translations. This class is
subclassed below for each translated string
in each language. '''

def testMatchingFormatting(self):
for formatter in '%s', '%d', '%.2f':
self.assertEqual(self.englishString.count(formatter),
self.translatedString.count(formatter))

def testMatchingAccelerators(self):
# snipped


def getLanguages():
return [language for language in \
meta.data.languages.values() \
if language is not None]


def createTestCaseClassName(language, englishString,
prefix='TranslationIntegrityTest'):
''' Generate a class name for the test case class based
on the language and the English string. '''

# Make sure we only use characters allowed in Python
# identifiers:
englishString = englishString.replace(' ', '_')
allowableCharacters = string.ascii_letters + \
string.digits + '_'
englishString = ''.join([char for char in englishString \
if char in allowableCharacters])
className = '%s_%s_%s'%(prefix, language, englishString)
count = 0
while className in globals(): # Make sure className is unique
count += 1
className = '%s_%s_%s_%d'%(prefix, language,
englishString, count)
return className


def createTestCaseClass(className, language, englishString,
translatedString):
class_ = type(className,
(TranslationIntegrityTests, test.TestCase),
{})
class_.language = language
class_.englishString = englishString
class_.translatedString = translatedString
return class_


for language in getLanguages():
translation = __import__('i18n.%s'%language,
fromlist=['dict'])
for english, translated in translation.dict.iteritems():
className = createTestCaseClassName(language, english)
class_ = createTestCaseClass(className, language,
english, translated)
globals()[className] = class_

Saturday, May 26, 2007

False positive

A few days ago AVG Anti-virus started reporting a trojan horse in Task Coach (0.63.2). A little investigation with the help of other py2exe users indicates that AVG detects a trojan in a specific part of py2exe. Py2exe is a program that is used to bundle python source code and the python interpreter into an executable that can be easily installed on Windows machines and doesn't require users to install python. It seems that someone wrote a trojan in python and bundled it with py2exe. Apparently, AVG is now triggered by py2exe instead of a signature that is specific for that trojan horse. It probably means that all applications bundled with py2exe are affected as well. What a bummer. But also kind of interesting to see how other applications, that have nothing to do with Task Coach itself, can cause bug reports about Task Coach.