From: Mark Hymers Date: Thu, 29 Oct 2009 14:14:52 +0000 (+0000) Subject: Merge commit 'ftpmaster/master' X-Git-Url: https://git.decadent.org.uk/gitweb/?a=commitdiff_plain;h=f1ee050ceee00693a9bea2673431291ac480b8b6;hp=b10c8f22028028dfe86d6d8a83b030b08464322e;p=dak.git Merge commit 'ftpmaster/master' --- diff --git a/config/debian/cron.dinstall b/config/debian/cron.dinstall index 1c9fa5af..200c7a64 100755 --- a/config/debian/cron.dinstall +++ b/config/debian/cron.dinstall @@ -197,9 +197,6 @@ function accepted() { function cruft() { log "Checking for cruft in overrides" dak check-overrides - - log "Fixing symlinks in $ftpdir" - symlinks -d -r $ftpdir } function msfl() { @@ -252,6 +249,7 @@ function mpfm() { function packages() { log "Generating Packages and Sources files" cd $configdir + GZIP='--rsyncable' ; export GZIP apt-ftparchive generate apt.conf } @@ -905,14 +903,6 @@ GO=( ) stage $GO & -GO=( - FUNC="aptftpcleanup" - TIME="apt-ftparchive cleanup" - ARGS="" - ERR="false" -) -stage $GO & - GO=( FUNC="merkel3" TIME="merkel ddaccessible sync" @@ -927,6 +917,14 @@ GO=( ARGS="" ERR="" ) +stage $GO & + +GO=( + FUNC="aptftpcleanup" + TIME="apt-ftparchive cleanup" + ARGS="" + ERR="false" +) stage $GO log "Daily cron scripts successful, all done" diff --git a/config/debian/cron.weekly b/config/debian/cron.weekly index 5ab9d8b8..34f0c64a 100755 --- a/config/debian/cron.weekly +++ b/config/debian/cron.weekly @@ -57,6 +57,9 @@ apt-ftparchive -q clean apt.conf.buildd echo "Update wanna-build database dump" /org/ftp.debian.org/scripts/nfu/get-w-b-db +echo "Fixing symlinks in $ftpdir" +symlinks -d -r $ftpdir + echo "Finally, all is done, compressing logfile" exec > /dev/null 2>&1 diff --git a/dak/dak.py b/dak/dak.py index 052f3b3e..19facc55 100755 --- a/dak/dak.py +++ b/dak/dak.py @@ -34,8 +34,12 @@ G{importgraph} ################################################################################ import sys +import traceback import daklib.utils +from daklib.daklog import Logger +from daklib.config import Config + ################################################################################ def init(): @@ -152,6 +156,8 @@ Available commands:""" def main(): """Launch dak functionality.""" + logger = Logger(Config(), 'dak top-level', print_starting=False) + functionality = init() modules = [ command for (command, _) in functionality ] @@ -189,7 +195,19 @@ def main(): # Invoke the module module = __import__(cmdname.replace("-","_")) - module.main() + try: + module.main() + except KeyboardInterrupt: + msg = 'KeyboardInterrupt caught; exiting' + print msg + logger.log([msg]) + sys.exit(1) + except SystemExit: + pass + except: + for line in traceback.format_exc().split('\n')[:-1]: + logger.log(['exception', line]) + raise ################################################################################ diff --git a/dak/generate_index_diffs.py b/dak/generate_index_diffs.py index 4222c0cf..7e4b0058 100755 --- a/dak/generate_index_diffs.py +++ b/dak/generate_index_diffs.py @@ -254,7 +254,7 @@ def genchanges(Options, outdir, oldfile, origfile, maxdiffs = 14): if not os.path.isdir(outdir): os.mkdir(outdir) - w = os.popen("diff --ed - %s | gzip -c -9 > %s.gz" % + w = os.popen("diff --ed - %s | gzip --rsyncable -c -9 > %s.gz" % (newfile, difffile), "w") pipe_file(oldf, w) oldf.close() diff --git a/dak/process_unchecked.py b/dak/process_unchecked.py index 5688e83f..db29ac42 100755 --- a/dak/process_unchecked.py +++ b/dak/process_unchecked.py @@ -497,7 +497,7 @@ def process_it(changes_file): u.check_distributions() u.check_files(not Options["No-Action"]) valid_dsc_p = u.check_dsc(not Options["No-Action"]) - if valid_dsc_p: + if valid_dsc_p and not Options["No-Action"]: u.check_source() u.check_lintian() u.check_hashes() @@ -507,7 +507,7 @@ def process_it(changes_file): action(u) - except SystemExit: + except (SystemExit, KeyboardInterrupt): raise except: diff --git a/daklib/daklog.py b/daklib/daklog.py index dfcae368..fb33b0bd 100755 --- a/daklib/daklog.py +++ b/daklib/daklog.py @@ -38,7 +38,7 @@ class Logger: logfile = None program = None - def __init__ (self, Cnf, program, debug=0): + def __init__ (self, Cnf, program, debug=0, print_starting=True): "Initialize a new Logger object" self.Cnf = Cnf self.program = program @@ -58,7 +58,8 @@ class Logger: logfile = utils.open_file(logfilename, 'a') os.umask(umask) self.logfile = logfile - self.log(["program start"]) + if print_starting: + self.log(["program start"]) def log (self, details): "Log an event" diff --git a/daklib/formats.py b/daklib/formats.py new file mode 100644 index 00000000..aaad2715 --- /dev/null +++ b/daklib/formats.py @@ -0,0 +1,45 @@ +from regexes import re_verwithext +from dak_exceptions import UnknownFormatError + +def parse_format(txt): + """ + Parse a .changes Format string into a tuple representation for easy + comparison. + + >>> parse_format('1.0') + (1, 0) + >>> parse_format('8.4 (hardy)') + (8, 4, 'hardy') + + If the format doesn't match these forms, raises UnknownFormatError. + """ + + format = re_verwithext.search(txt) + + if format is None: + raise UnknownFormatError, txt + + format = format.groups() + + if format[1] is None: + format = int(float(format[0])), 0, format[2] + else: + format = int(format[0]), int(format[1]), format[2] + + if format[2] is None: + format = format[:2] + + return format + +def validate_changes_format(format, field): + """ + Validate a tuple-representation of a .changes Format: field. Raises + UnknownFormatError if the field is invalid, otherwise return type is + undefined. + """ + + if (format < (1, 5) or format > (1, 8)): + raise UnknownFormatError, repr(format) + + if field != 'files' and format < (1, 8): + raise UnknownFormatError, repr(format) diff --git a/daklib/queue.py b/daklib/queue.py index 7e93448c..e6547f7e 100755 --- a/daklib/queue.py +++ b/daklib/queue.py @@ -1036,8 +1036,8 @@ class Upload(object): if not os.path.exists(src): return ftype = m.group(3) - if re_is_orig_source.match(f) and pkg.orig_files.has_key(f) and \ - pkg.orig_files[f].has_key("path"): + if re_is_orig_source.match(f) and self.pkg.orig_files.has_key(f) and \ + self.pkg.orig_files[f].has_key("path"): continue dest = os.path.join(os.getcwd(), f) os.symlink(src, dest) @@ -1183,6 +1183,8 @@ class Upload(object): ########################################################################### def check_lintian(self): + cnf = Config() + # Only check some distributions valid_dist = False for dist in ('unstable', 'experimental'): @@ -1193,11 +1195,11 @@ class Upload(object): if not valid_dist: return - cnf = Config() tagfile = cnf.get("Dinstall::LintianTags") if tagfile is None: # We don't have a tagfile, so just don't do anything. return + # Parse the yaml file sourcefile = file(tagfile, 'r') sourcecontent = sourcefile.read() @@ -1208,6 +1210,73 @@ class Upload(object): utils.fubar("Can not read the lintian tags file %s, YAML error: %s." % (tagfile, msg)) return + # Try and find all orig mentioned in the .dsc + target_dir = '.' + symlinked = [] + for filename, entry in self.pkg.dsc_files.iteritems(): + if not re_is_orig_source.match(filename): + # File is not an orig; ignore + continue + + if os.path.exists(filename): + # File exists, no need to continue + continue + + def symlink_if_valid(path): + f = utils.open_file(path) + md5sum = apt_pkg.md5sum(f) + f.close() + + fingerprint = (os.stat(path)[stat.ST_SIZE], md5sum) + expected = (int(entry['size']), entry['md5sum']) + + if fingerprint != expected: + return False + + dest = os.path.join(target_dir, filename) + + os.symlink(path, dest) + symlinked.append(dest) + + return True + + session = DBConn().session() + found = False + + # Look in the pool + for poolfile in get_poolfile_like_name('/%s' % filename, session): + poolfile_path = os.path.join( + poolfile.location.path, poolfile.filename + ) + + if symlink_if_valid(poolfile_path): + found = True + break + + session.close() + + if found: + continue + + # Look in some other queues for the file + queues = ('Accepted', 'New', 'Byhand', 'ProposedUpdates', + 'OldProposedUpdates', 'Embargoed', 'Unembargoed') + + for queue in queues: + if 'Dir::Queue::%s' % directory not in cnf: + continue + + queuefile_path = os.path.join( + cnf['Dir::Queue::%s' % directory], filename + ) + + if not os.path.exists(queuefile_path): + # Does not exist in this queue + continue + + if symlink_if_valid(queuefile_path): + break + # Now setup the input file for lintian. lintian wants "one tag per line" only, # so put it together like it. We put all types of tags in one file and then sort # through lintians output later to see if its a fatal tag we detected, or not. @@ -1227,8 +1296,12 @@ class Upload(object): # to then parse it. command = "lintian --show-overrides --tags-from-file %s %s" % (temp_filename, self.pkg.changes_file) (result, output) = commands.getstatusoutput(command) - # We are done with lintian, remove our tempfile + + # We are done with lintian, remove our tempfile and any symlinks we created os.unlink(temp_filename) + for symlink in symlinked: + os.unlink(symlink) + if (result == 2): utils.warn("lintian failed for %s [return code: %s]." % (self.pkg.changes_file, result)) utils.warn(utils.prefix_multi_line_string(output, " [possible output:] ")) @@ -1236,6 +1309,10 @@ class Upload(object): if len(output) == 0: return + def log(*txt): + if self.logger: + self.logger.log([self.pkg.changes_file, "check_lintian"] + list(txt)) + # We have output of lintian, this package isn't clean. Lets parse it and see if we # are having a victim for a reject. # W: tzdata: binary-without-manpage usr/sbin/tzconfig @@ -1262,9 +1339,11 @@ class Upload(object): elif etag in lintiantags['error']: # The tag is overriden - but is not allowed to be self.rejects.append("%s: Overriden tag %s found, but this tag may not be overwritten." % (epackage, etag)) + log("overidden tag is overridden", etag) else: # Tag is known, it is not overriden, direct reject. self.rejects.append("%s: Found lintian output: '%s %s', automatically rejected package." % (epackage, etag, etext)) + log("auto rejecting", etag) # Now tell if they *might* override it. if etag in lintiantags['warning']: self.rejects.append("%s: If you have a good reason, you may override this lintian tag." % (epackage)) diff --git a/daklib/srcformats.py b/daklib/srcformats.py index ade3c453..7d7dd940 100644 --- a/daklib/srcformats.py +++ b/daklib/srcformats.py @@ -1,6 +1,5 @@ import re -from regexes import re_verwithext from dak_exceptions import UnknownFormatError srcformats = [] @@ -18,36 +17,6 @@ def get_format_from_string(txt): raise UnknownFormatError, "Unknown format %r" % txt -def parse_format(txt): - """ - Parse a .changes Format string into a tuple representation for easy - comparison. - - >>> parse_format('1.0') - (1, 0) - >>> parse_format('8.4 (hardy)') - (8, 4, 'hardy') - - If the format doesn't match these forms, raises UnknownFormatError. - """ - - format = re_verwithext.search(txt) - - if format is None: - raise UnknownFormatError, txt - - format = format.groups() - - if format[1] is None: - format = int(float(format[0])), 0, format[2] - else: - format = int(format[0]), int(format[1]), format[2] - - if format[2] is None: - format = format[:2] - - return format - class SourceFormat(type): def __new__(cls, name, bases, attrs): klass = super(SourceFormat, cls).__new__(cls, name, bases, attrs) @@ -70,15 +39,6 @@ class SourceFormat(type): if has[key]: yield "contains source files not allowed in format %s" % cls.name - @classmethod - def validate_format(cls, format, is_a_dsc=False, field='files'): - """ - Raises UnknownFormatError if the specified format tuple is not valid for - this format (for example, the format (1, 0) is not valid for the - "3.0 (quilt)" format). Return value is undefined in all other cases. - """ - pass - class FormatOne(SourceFormat): __metaclass__ = SourceFormat @@ -101,19 +61,6 @@ class FormatOne(SourceFormat): for msg in super(FormatOne, cls).reject_msgs(has): yield msg - @classmethod - def validate_format(cls, format, is_a_dsc=False, field='files'): - msg = "Invalid format %s definition: %r" % (cls.name, format) - - if is_a_dsc: - if format != (1, 0): - raise UnknownFormatError, msg - else: - if (format < (1,5) or format > (1,8)): - raise UnknownFormatError, msg - if field != "files" and format < (1,8): - raise UnknownFormatError, msg - class FormatThree(SourceFormat): __metaclass__ = SourceFormat @@ -123,12 +70,6 @@ class FormatThree(SourceFormat): requires = ('native_tar',) disallowed = ('orig_tar', 'debian_diff', 'debian_tar', 'more_orig_tar') - @classmethod - def validate_format(cls, format, **kwargs): - if format != (3, 0, 'native'): - raise UnknownFormatError, "Invalid format %s definition: %r" % \ - (cls.name, format) - class FormatThreeQuilt(SourceFormat): __metaclass__ = SourceFormat @@ -137,9 +78,3 @@ class FormatThreeQuilt(SourceFormat): requires = ('orig_tar', 'debian_tar') disallowed = ('debian_diff', 'native_tar') - - @classmethod - def validate_format(cls, format, **kwargs): - if format != (3, 0, 'quilt'): - raise UnknownFormatError, "Invalid format %s definition: %r" % \ - (cls.name, format) diff --git a/daklib/utils.py b/daklib/utils.py index 2b243b85..a6660ae1 100755 --- a/daklib/utils.py +++ b/daklib/utils.py @@ -47,6 +47,7 @@ from regexes import re_html_escaping, html_escaping, re_single_line_field, \ re_multi_line_field, re_srchasver, re_taint_free, \ re_gpg_uid, re_re_mark, re_whitespace_comment, re_issource +from formats import parse_format, validate_changes_format from srcformats import get_format_from_string from collections import defaultdict @@ -526,9 +527,9 @@ def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"): if not changes.has_key(field): raise NoFilesFieldError - # Get SourceFormat object for this Format and validate it - format = get_format_from_string(changes.get['format']) - format.validate_format(is_a_dsc=is_a_dsc, field=field) + # Validate .changes Format: field + if not is_a_dsc: + validate_changes_format(parse_format(changes['format']), field) includes_section = (not is_a_dsc) and field == "files" @@ -1505,5 +1506,3 @@ apt_pkg.ReadConfigFileISC(Cnf,default_config) if which_conf_file() != default_config: apt_pkg.ReadConfigFileISC(Cnf,which_conf_file()) - -############################################################################### diff --git a/docs/README.quotes b/docs/README.quotes index e531a241..d6bd125b 100644 --- a/docs/README.quotes +++ b/docs/README.quotes @@ -367,3 +367,10 @@ Canadians: This is a lighthouse. Your call. mhy: Error: "!!!11111iiiiiioneoneoneone" is not a valid command. dak: oh shut up mhy: Error: "oh" is not a valid command. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + hey, I think something's wrong with your git repo + when I git pulled this last time, I got something that looked almost like python instead of dak + sgran: slander + sorry, I take it back, I've had a better look now diff --git a/scripts/debian/byhand-di b/scripts/debian/byhand-di index 0a004f38..67db5516 100755 --- a/scripts/debian/byhand-di +++ b/scripts/debian/byhand-di @@ -95,6 +95,10 @@ mv "$TMPDIR/installer-$ARCH/current" "$TARGET" find "$TARGET/$VERSION" -type d -exec chmod 755 {} + find "$TARGET/$VERSION" -type f -exec chmod 644 {} + +# Make sure nothing symlinks outside of the ftpdir +# Shouldnt happen, but better be sure. +symlinks -d -r /srv/ftp.debian.org/ftp + trap - EXIT cleanup diff --git a/scripts/debian/mkfilesindices b/scripts/debian/mkfilesindices index b9d31a02..c16fde6a 100755 --- a/scripts/debian/mkfilesindices +++ b/scripts/debian/mkfilesindices @@ -27,7 +27,7 @@ echo "Generating sources list..." cd $base/ftp find ./dists -maxdepth 1 \! -type d find ./dists \! -type d | grep "/source/" -) | sort -u | gzip -9 > source.list.gz +) | sort -u | gzip --rsyncable -9 > source.list.gz echo "Generating arch lists..." @@ -39,7 +39,7 @@ for a in $ARCHES; do cd $base/ftp find ./dists -maxdepth 1 \! -type d find ./dists \! -type d | grep -E "(proposed-updates.*_$a.changes$|/main/disks-$a/|/main/installer-$a/|/Contents-$a|/binary-$a/)" - ) | sort -u | gzip -9 > arch-$a.list.gz + ) | sort -u | gzip --rsyncable -9 > arch-$a.list.gz done echo "Generating suite lists..." @@ -62,7 +62,7 @@ printf 'SELECT id, suite_name FROM suite\n' | psql -F' ' -At projectb | done ) suite_list $id | tr -d ' ' | sed 's,^/srv/ftp.debian.org/ftp,.,' - ) | sort -u | gzip -9 > suite-${suite}.list.gz + ) | sort -u | gzip --rsyncable -9 > suite-${suite}.list.gz done echo "Finding everything on the ftp site to generate sundries $(date +"%X")..." @@ -83,7 +83,7 @@ done (cd $base/ftp/ for dist in sid squeeze; do - find ./dists/$dist/main/i18n/ \! -type d | sort -u | gzip -9 > $base/ftp/indices/files/components/translation-$dist.list.gz + find ./dists/$dist/main/i18n/ \! -type d | sort -u | gzip --rsyncable -9 > $base/ftp/indices/files/components/translation-$dist.list.gz done ) diff --git a/scripts/debian/mklslar b/scripts/debian/mklslar index 19363f1f..231f7f8c 100755 --- a/scripts/debian/mklslar +++ b/scripts/debian/mklslar @@ -26,11 +26,11 @@ if [ -r ${filename}.gz ] ; then mv -f ${filename}.gz $filename.old.gz mv -f .$filename.new $filename rm -f $filename.patch.gz - zcat $filename.old.gz | diff -u - $filename | gzip -9cfn - >$filename.patch.gz + zcat $filename.old.gz | diff -u - $filename | gzip --rsyncable -9cfn - >$filename.patch.gz rm -f $filename.old.gz else mv -f .$filename.new $filename fi -gzip -9cfN $filename >$filename.gz +gzip --rsyncable -9cfN $filename >$filename.gz rm -f $filename diff --git a/scripts/debian/mkmaintainers b/scripts/debian/mkmaintainers index a0abaa1f..41e8727c 100755 --- a/scripts/debian/mkmaintainers +++ b/scripts/debian/mkmaintainers @@ -17,7 +17,7 @@ set -e if [ $rc = 1 ] || [ ! -f Maintainers ] ; then echo -n "installing Maintainers ... " mv -f .new-maintainers Maintainers - gzip -9v .new-maintainers.gz + gzip --rsyncable -9v .new-maintainers.gz mv -f .new-maintainers.gz Maintainers.gz elif [ $rc = 0 ] ; then echo '(same as before)' diff --git a/tests/test_formats.py b/tests/test_formats.py new file mode 100755 index 00000000..1ae6860a --- /dev/null +++ b/tests/test_formats.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +import unittest + +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from daklib.formats import parse_format, validate_changes_format +from daklib.dak_exceptions import UnknownFormatError + +class ParseFormatTestCase(unittest.TestCase): + def assertParse(self, format, expected): + self.assertEqual(parse_format(format), expected) + + def assertParseFail(self, format): + self.assertRaises( + UnknownFormatError, + lambda: parse_format(format) + ) + + def testParse(self): + self.assertParse('1.0', (1, 0)) + + def testEmpty(self): + self.assertParseFail('') + self.assertParseFail(' ') + self.assertParseFail(' ') + + def textText(self): + self.assertParse('1.2 (three)', (1, 2, 'three')) + self.assertParseFail('0.0 ()') + +class ValidateChangesFormat(unittest.TestCase): + def assertValid(self, changes, field='files'): + validate_changes_format(changes, field) + + def assertInvalid(self, *args, **kwargs): + self.assertRaises( + UnknownFormatError, + lambda: self.assertValid(*args, **kwargs) + ) + + ## + + def testBinary(self): + self.assertValid((1, 5)) + self.assertValid((1, 8)) + self.assertInvalid((1, 0)) + + def testRange(self): + self.assertInvalid((1, 3)) + self.assertValid((1, 5)) + self.assertValid((1, 8)) + self.assertInvalid((1, 9)) + + def testFilesField(self): + self.assertInvalid((1, 7), field='notfiles') + self.assertValid((1, 8), field='notfiles') diff --git a/tests/test_srcformats.py b/tests/test_srcformats.py index f6d7215f..4ecaf8b7 100755 --- a/tests/test_srcformats.py +++ b/tests/test_srcformats.py @@ -8,6 +8,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from collections import defaultdict from daklib import srcformats +from daklib.formats import parse_format from daklib.dak_exceptions import UnknownFormatError class SourceFormatTestCase(unittest.TestCase): @@ -103,89 +104,6 @@ class FormatTreeQuiltTestCase(SourceFormatTestCase): 'native_tar': 1, }) -## - -class ParseFormatTestCase(unittest.TestCase): - def assertParse(self, format, expected): - self.assertEqual(srcformats.parse_format(format), expected) - - def assertParseFail(self, format): - self.assertRaises( - UnknownFormatError, - lambda: srcformats.parse_format(format) - ) - - def testParse(self): - self.assertParse('1.0', (1, 0)) - - def testEmpty(self): - self.assertParseFail('') - self.assertParseFail(' ') - self.assertParseFail(' ') - - def textText(self): - self.assertParse('1.2 (three)', (1, 2, 'three')) - self.assertParseFail('0.0 ()') - -class ValidateFormatTestCase(unittest.TestCase): - def assertValid(self, format, **kwargs): - kwargs['is_a_dsc'] = kwargs.get('is_a_dsc', True) - self.fmt.validate_format(format, **kwargs) - - def assertInvalid(self, *args, **kwargs): - self.assertRaises( - UnknownFormatError, - lambda: self.assertValid(*args, **kwargs), - ) - -class ValidateFormatOneTestCase(ValidateFormatTestCase): - fmt = srcformats.FormatOne - - def testValid(self): - self.assertValid((1, 0)) - - def testInvalid(self): - self.assertInvalid((0, 1)) - self.assertInvalid((3, 0, 'quilt')) - - ## - - def testBinary(self): - self.assertValid((1, 5), is_a_dsc=False) - self.assertInvalid((1, 0), is_a_dsc=False) - - def testRange(self): - self.assertInvalid((1, 3), is_a_dsc=False) - self.assertValid((1, 5), is_a_dsc=False) - self.assertValid((1, 8), is_a_dsc=False) - self.assertInvalid((1, 9), is_a_dsc=False) - - def testFilesField(self): - self.assertInvalid((1, 7), is_a_dsc=False, field='notfiles') - self.assertValid((1, 8), is_a_dsc=False, field='notfiles') - -class ValidateFormatThreeTestCase(ValidateFormatTestCase): - fmt = srcformats.FormatThree - - def testValid(self): - self.assertValid((3, 0, 'native')) - - def testInvalid(self): - self.assertInvalid((1, 0)) - self.assertInvalid((0, 0)) - self.assertInvalid((3, 0, 'quilt')) - -class ValidateFormatThreeQuiltTestCase(ValidateFormatTestCase): - fmt = srcformats.FormatThreeQuilt - - def testValid(self): - self.assertValid((3, 0, 'quilt')) - - def testInvalid(self): - self.assertInvalid((1, 0)) - self.assertInvalid((0, 0)) - self.assertInvalid((3, 0, 'native')) - class FormatFromStringTestCase(unittest.TestCase): def assertFormat(self, txt, klass): self.assertEqual(srcformats.get_format_from_string(txt), klass)