forked from upstream-mirrors/OpenTracks
Add a class EndToEndTestRunner which extends InstrumentationTestRunner and can get the port of emulator. Then test can be run on more than one emulators at the same time.
This commit is contained in:
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This script retrieves the value of a given named string from all indicated
|
||||
# strings.xml files. If invoked in a project directory (a directory with a
|
||||
# 'res' subdirectory), and if no strings.xml files are provided, the script
|
||||
# will automatically analyze res/values*/strings.xml
|
||||
|
||||
PROGNAME=$(basename "$0")
|
||||
|
||||
function usage() {
|
||||
echo "Usage: ${PROGNAME} string_name [file file..]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
function die() {
|
||||
echo "${PROGNAME}: $@" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "$#" -lt 1 ]] ; then
|
||||
usage
|
||||
fi
|
||||
|
||||
name=$1
|
||||
shift
|
||||
|
||||
files=
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
if [[ -d res ]] ; then
|
||||
files=res/values*/strings.xml
|
||||
else
|
||||
die "invoked outside of project root with no file arguments"
|
||||
fi
|
||||
else
|
||||
files="$@"
|
||||
fi
|
||||
|
||||
for file in $files ; do
|
||||
echo === $file
|
||||
xmllint --xpath /resources/string[@name=\"$name\"] $file
|
||||
echo
|
||||
done
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>i18n</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.python.pydev.PyDevBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.python.pydev.pythonNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?eclipse-pydev version="1.0"?>
|
||||
|
||||
<pydev_project>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/i18n/src</path>
|
||||
</pydev_pathproperty>
|
||||
</pydev_project>
|
||||
@@ -0,0 +1,45 @@
|
||||
'''
|
||||
Module for dealing with resource files (but not their contents).
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
import os.path
|
||||
from glob import glob
|
||||
import re
|
||||
|
||||
MYTRACKS_RES_DIR = 'MyTracks/res'
|
||||
ANDROID_MASTER_VALUES = 'values'
|
||||
ANDROID_VALUES_MASK = 'values-*'
|
||||
|
||||
|
||||
def GetMyTracksDir():
|
||||
'''
|
||||
Returns the directory in which the MyTracks directory is located.
|
||||
'''
|
||||
path = os.getcwd()
|
||||
while not os.path.isdir(os.path.join(path, MYTRACKS_RES_DIR)):
|
||||
if path == '/':
|
||||
raise 'Not in My Tracks project'
|
||||
|
||||
# Go up one level
|
||||
path = os.path.split(path)[0]
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def GetAllLanguageFiles():
|
||||
'''
|
||||
Returns a mapping from all found languages to their respective directories.
|
||||
'''
|
||||
mytracks_path = GetMyTracksDir()
|
||||
res_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_VALUES_MASK)
|
||||
language_dirs = glob(res_dir)
|
||||
master_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_MASTER_VALUES)
|
||||
if len(language_dirs) == 0:
|
||||
raise 'No languages found!'
|
||||
if not os.path.isdir(master_dir):
|
||||
raise 'Couldn\'t find master file'
|
||||
|
||||
language_tuples = [(re.findall(r'.*values-([A-Za-z-]+)', dir)[0],dir) for dir in language_dirs]
|
||||
language_tuples.append(('en', master_dir))
|
||||
return dict(language_tuples)
|
||||
@@ -0,0 +1,94 @@
|
||||
'''
|
||||
Module which brings history information about files from Mercurial.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
REVISION_REGEX = re.compile(r'(?P<hash>[0-9a-f]{12}):.*')
|
||||
|
||||
def _GetOutputLines(args):
|
||||
'''
|
||||
Runs an external process and returns its output as a list of lines.
|
||||
|
||||
@param args: the arguments to run
|
||||
'''
|
||||
process = subprocess.Popen(args,
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines = True,
|
||||
shell = False)
|
||||
output = process.communicate()[0]
|
||||
return output.splitlines()
|
||||
|
||||
|
||||
def FillMercurialRevisions(filename, parsed_file):
|
||||
'''
|
||||
Fills the revs attribute of all strings in the given parsed file with
|
||||
a list of revisions that touched the lines corresponding to that string.
|
||||
|
||||
@param filename: the name of the file to get history for
|
||||
@param parsed_file: the parsed file to modify
|
||||
'''
|
||||
# Take output of hg annotate to get revision of each line
|
||||
output_lines = _GetOutputLines(['hg', 'annotate', '-c', filename])
|
||||
|
||||
# Create a map of line -> revision (key is list index, line 0 doesn't exist)
|
||||
line_revs = ['dummy']
|
||||
for line in output_lines:
|
||||
rev_match = REVISION_REGEX.match(line)
|
||||
if not rev_match:
|
||||
raise 'Unexpected line of output from hg: %s' % line
|
||||
rev_hash = rev_match.group('hash')
|
||||
line_revs.append(rev_hash)
|
||||
|
||||
for str in parsed_file.itervalues():
|
||||
# Get the lines that correspond to each string
|
||||
start_line = str['startLine']
|
||||
end_line = str['endLine']
|
||||
|
||||
# Get the revisions that touched those lines
|
||||
revs = []
|
||||
for line_number in range(start_line, end_line + 1):
|
||||
revs.append(line_revs[line_number])
|
||||
|
||||
# Merge with any revisions that were already there
|
||||
# (for explict revision specification)
|
||||
if 'revs' in str:
|
||||
revs += str['revs']
|
||||
|
||||
# Assign the revisions to the string
|
||||
str['revs'] = frozenset(revs)
|
||||
|
||||
def DoesRevisionSuperceed(filename, rev1, rev2):
|
||||
'''
|
||||
Tells whether a revision superceeds another.
|
||||
This essentially means that the older revision is an ancestor of the newer
|
||||
one.
|
||||
This also returns True if the two revisions are the same.
|
||||
|
||||
@param rev1: the revision that may be superceeding the other
|
||||
@param rev2: the revision that may be superceeded
|
||||
@return: True if rev1 superceeds rev2 or they're the same
|
||||
'''
|
||||
if rev1 == rev2:
|
||||
return True
|
||||
|
||||
# TODO: Add filename
|
||||
args = ['hg', 'log', '-r', 'ancestors(%s)' % rev1, '--template', '{node|short}\n', filename]
|
||||
output_lines = _GetOutputLines(args)
|
||||
|
||||
return rev2 in output_lines
|
||||
|
||||
def NewestRevision(filename, rev1, rev2):
|
||||
'''
|
||||
Returns which of two revisions is closest to the head of the repository.
|
||||
If none of them is the ancestor of the other, then we return either one.
|
||||
|
||||
@param rev1: the first revision
|
||||
@param rev2: the second revision
|
||||
'''
|
||||
if DoesRevisionSuperceed(filename, rev1, rev2):
|
||||
return rev1
|
||||
return rev2
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/python
|
||||
'''
|
||||
Entry point for My Tracks i18n tool.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import mytracks.files
|
||||
import mytracks.translate
|
||||
import mytracks.validate
|
||||
import sys
|
||||
|
||||
def Usage():
|
||||
print 'Usage: %s <command> [<language> ...]\n' % sys.argv[0]
|
||||
print 'Commands are:'
|
||||
print ' cleanup'
|
||||
print ' translate'
|
||||
print ' validate'
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def Translate(languages):
|
||||
'''
|
||||
Asks the user to interactively translate any missing or oudated strings from
|
||||
the files for the given languages.
|
||||
|
||||
@param languages: the languages to translate
|
||||
'''
|
||||
validator = mytracks.validate.Validator(languages)
|
||||
validator.Validate()
|
||||
missing = validator.missing_in_lang()
|
||||
outdated = validator.outdated_in_lang()
|
||||
|
||||
for lang in languages:
|
||||
untranslated = missing[lang] + outdated[lang]
|
||||
|
||||
if len(untranslated) == 0:
|
||||
continue
|
||||
|
||||
translator = mytracks.translate.Translator(lang)
|
||||
translator.Translate(untranslated)
|
||||
|
||||
|
||||
def Validate(languages):
|
||||
'''
|
||||
Computes and displays errors in the string files for the given languages.
|
||||
|
||||
@param languages: the languages to compute for
|
||||
'''
|
||||
validator = mytracks.validate.Validator(languages)
|
||||
validator.Validate()
|
||||
|
||||
error_count = 0
|
||||
if (validator.valid()):
|
||||
print 'All files OK'
|
||||
else:
|
||||
for lang, missing in validator.missing_in_master().iteritems():
|
||||
print 'Missing in master, present in %s: %s:' % (lang, str(missing))
|
||||
error_count = error_count + len(missing)
|
||||
for lang, missing in validator.missing_in_lang().iteritems():
|
||||
print 'Missing in %s, present in master: %s:' % (lang, str(missing))
|
||||
error_count = error_count + len(missing)
|
||||
for lang, outdated in validator.outdated_in_lang().iteritems():
|
||||
print 'Outdated in %s: %s:' % (lang, str(outdated))
|
||||
error_count = error_count + len(outdated)
|
||||
|
||||
return error_count
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argv = sys.argv
|
||||
argc = len(argv)
|
||||
if argc < 2:
|
||||
Usage()
|
||||
|
||||
languages = mytracks.files.GetAllLanguageFiles()
|
||||
if argc == 3:
|
||||
langs = set(argv[2:])
|
||||
if not langs.issubset(languages):
|
||||
raise 'Language(s) not found'
|
||||
|
||||
# Filter just to the languages specified
|
||||
languages = dict((lang, lang_file)
|
||||
for lang, lang_file in languages.iteritems()
|
||||
if lang in langs or lang == 'en' )
|
||||
|
||||
cmd = argv[1]
|
||||
if cmd == 'translate':
|
||||
Translate(languages)
|
||||
elif cmd == 'validate':
|
||||
error_count = Validate(languages)
|
||||
else:
|
||||
Usage()
|
||||
error_count = 0
|
||||
|
||||
print '%d errors found.' % error_count
|
||||
@@ -0,0 +1,115 @@
|
||||
'''
|
||||
Module which parses a string XML file.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
from xml.parsers.expat import ParserCreate
|
||||
import re
|
||||
#import xml.etree.ElementTree as ET
|
||||
|
||||
class StringsParser(object):
|
||||
'''
|
||||
Parser for string XML files.
|
||||
|
||||
This object is not thread-safe and should be used for parsing a single file at
|
||||
a time, only.
|
||||
'''
|
||||
|
||||
def Parse(self, file):
|
||||
'''
|
||||
Parses the given file and returns a dictionary mapping keys to an object
|
||||
with attributes for that key, such as the value, start/end line and explicit
|
||||
revisions.
|
||||
|
||||
In addition to the standard XML format of the strings file, this parser
|
||||
supports an annotation inside comments, in one of these formats:
|
||||
|
||||
<!-- KEEP_PARENT name="bla" -->
|
||||
<!-- KEEP_PARENT name="bla" rev="123456789012" -->
|
||||
|
||||
Such an annotation indicates that we're explicitly inheriting form the
|
||||
master file (and the optional revision says that this decision is compatible
|
||||
with the master file up to that revision).
|
||||
|
||||
@param file: the name of the file to parse
|
||||
'''
|
||||
self._Reset()
|
||||
|
||||
# Unfortunately expat is the only parser that will give us line numbers
|
||||
self._xml_parser = ParserCreate()
|
||||
self._xml_parser.StartElementHandler = self._StartElementHandler
|
||||
self._xml_parser.EndElementHandler = self._EndElementHandler
|
||||
self._xml_parser.CharacterDataHandler = self._CharacterDataHandler
|
||||
self._xml_parser.CommentHandler = self._CommentHandler
|
||||
|
||||
file_obj = open(file)
|
||||
self._xml_parser.ParseFile(file_obj)
|
||||
file_obj.close()
|
||||
|
||||
return self._all_strings
|
||||
|
||||
def _Reset(self):
|
||||
self._currentString = None
|
||||
self._currentStringName = None
|
||||
self._currentStringValue = None
|
||||
self._all_strings = {}
|
||||
|
||||
def _StartElementHandler(self, name, attrs):
|
||||
if name != 'string':
|
||||
return
|
||||
|
||||
if 'name' not in attrs:
|
||||
return
|
||||
|
||||
assert not self._currentString
|
||||
assert not self._currentStringName
|
||||
self._currentString = {
|
||||
'startLine' : self._xml_parser.CurrentLineNumber,
|
||||
}
|
||||
|
||||
if 'rev' in attrs:
|
||||
self._currentString['revs'] = [attrs['rev']]
|
||||
|
||||
self._currentStringName = attrs['name']
|
||||
self._currentStringValue = ''
|
||||
|
||||
def _EndElementHandler(self, name):
|
||||
if name != 'string':
|
||||
return
|
||||
|
||||
assert self._currentString
|
||||
assert self._currentStringName
|
||||
self._currentString['value'] = self._currentStringValue
|
||||
self._currentString['endLine'] = self._xml_parser.CurrentLineNumber
|
||||
self._all_strings[self._currentStringName] = self._currentString
|
||||
|
||||
self._currentString = None
|
||||
self._currentStringName = None
|
||||
self._currentStringValue = None
|
||||
|
||||
def _CharacterDataHandler(self, data):
|
||||
if not self._currentString:
|
||||
return
|
||||
|
||||
self._currentStringValue += data
|
||||
|
||||
_KEEP_PARENT_REGEX = re.compile(r'\s*KEEP_PARENT\s+'
|
||||
r'name\s*=\s*[\'"]?(?P<name>[a-z0-9_]+)[\'"]?'
|
||||
r'(?:\s+rev=[\'"]?(?P<rev>[0-9a-f]{12})[\'"]?)?\s*',
|
||||
re.MULTILINE | re.DOTALL)
|
||||
|
||||
def _CommentHandler(self, data):
|
||||
keep_parent_match = self._KEEP_PARENT_REGEX.match(data)
|
||||
if not keep_parent_match:
|
||||
return
|
||||
|
||||
name = keep_parent_match.group('name')
|
||||
self._all_strings[name] = {
|
||||
'keepParent' : True,
|
||||
'startLine' : self._xml_parser.CurrentLineNumber,
|
||||
'endLine' : self._xml_parser.CurrentLineNumber
|
||||
}
|
||||
rev = keep_parent_match.group('rev')
|
||||
if rev:
|
||||
self._all_strings[name]['revs'] = [rev]
|
||||
@@ -0,0 +1,21 @@
|
||||
'''
|
||||
Module which prompts the user for translations and saves them.
|
||||
|
||||
TODO: implement
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
class Translator(object):
|
||||
'''
|
||||
classdocs
|
||||
'''
|
||||
|
||||
def __init__(self, language):
|
||||
'''
|
||||
Constructor
|
||||
'''
|
||||
self._language = language
|
||||
|
||||
def Translate(self, string_names):
|
||||
print string_names
|
||||
@@ -0,0 +1,115 @@
|
||||
'''
|
||||
Module which compares languague files to the master file and detects
|
||||
issues.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import os
|
||||
from mytracks.parser import StringsParser
|
||||
import mytracks.history
|
||||
|
||||
class Validator(object):
|
||||
|
||||
def __init__(self, languages):
|
||||
'''
|
||||
Builds a strings file validator.
|
||||
|
||||
Params:
|
||||
@param languages: a dictionary mapping each language to its corresponding directory
|
||||
'''
|
||||
self._langs = {}
|
||||
self._master = None
|
||||
self._language_paths = languages
|
||||
|
||||
parser = StringsParser()
|
||||
for lang, lang_dir in languages.iteritems():
|
||||
filename = os.path.join(lang_dir, 'strings.xml')
|
||||
parsed_file = parser.Parse(filename)
|
||||
mytracks.history.FillMercurialRevisions(filename, parsed_file)
|
||||
|
||||
if lang == 'en':
|
||||
self._master = parsed_file
|
||||
else:
|
||||
self._langs[lang] = parsed_file
|
||||
|
||||
self._Reset()
|
||||
|
||||
def Validate(self):
|
||||
'''
|
||||
Computes whether all the data in the files for the given languages is valid.
|
||||
'''
|
||||
self._Reset()
|
||||
self._ValidateMissingKeys()
|
||||
self._ValidateOutdatedKeys()
|
||||
|
||||
def valid(self):
|
||||
return (len(self._missing_in_master) == 0 and
|
||||
len(self._missing_in_lang) == 0 and
|
||||
len(self._outdated_in_lang) == 0)
|
||||
|
||||
def missing_in_master(self):
|
||||
return self._missing_in_master
|
||||
|
||||
def missing_in_lang(self):
|
||||
return self._missing_in_lang
|
||||
|
||||
def outdated_in_lang(self):
|
||||
return self._outdated_in_lang
|
||||
|
||||
def _Reset(self):
|
||||
# These are maps from language to string name list
|
||||
self._missing_in_master = {}
|
||||
self._missing_in_lang = {}
|
||||
self._outdated_in_lang = {}
|
||||
|
||||
def _ValidateMissingKeys(self):
|
||||
'''
|
||||
Computes whether there are missing keys on either side.
|
||||
'''
|
||||
master_keys = frozenset(self._master.iterkeys())
|
||||
for lang, file in self._langs.iteritems():
|
||||
keys = frozenset(file.iterkeys())
|
||||
missing_in_master = keys - master_keys
|
||||
missing_in_lang = master_keys - keys
|
||||
|
||||
if len(missing_in_master) > 0:
|
||||
self._missing_in_master[lang] = missing_in_master
|
||||
if len(missing_in_lang) > 0:
|
||||
self._missing_in_lang[lang] = missing_in_lang
|
||||
|
||||
def _ValidateOutdatedKeys(self):
|
||||
'''
|
||||
Computers whether any of the language keys are outdated with relation to the
|
||||
master keys.
|
||||
'''
|
||||
for lang, file in self._langs.iteritems():
|
||||
outdated = []
|
||||
for key, str in file.iteritems():
|
||||
# Get all revisions that touched master and language files for this
|
||||
# string.
|
||||
master_str = self._master[key]
|
||||
master_revs = master_str['revs']
|
||||
lang_revs = str['revs']
|
||||
if not master_revs or not lang_revs:
|
||||
print 'WARNING: No revision for %s in %s' % (key, lang)
|
||||
continue
|
||||
|
||||
master_file = os.path.join(self._language_paths['en'], 'strings.xml')
|
||||
lang_file = os.path.join(self._language_paths[lang], 'strings.xml')
|
||||
|
||||
# Assume that the repository has a single head (TODO: check that),
|
||||
# and as such there is always one revision which superceeds all others.
|
||||
master_rev = reduce(
|
||||
lambda r1, r2: mytracks.history.NewestRevision(master_file, r1, r2),
|
||||
master_revs)
|
||||
lang_rev = reduce(
|
||||
lambda r1, r2: mytracks.history.NewestRevision(lang_file, r1, r2),
|
||||
lang_revs)
|
||||
|
||||
# If the master version is newer than the lang version
|
||||
if mytracks.history.DoesRevisionSuperceed(lang_file, master_rev, lang_rev):
|
||||
outdated.append(key)
|
||||
|
||||
if len(outdated) > 0:
|
||||
self._outdated_in_lang[lang] = outdated
|
||||
Reference in New Issue
Block a user