diff options
author | Guilherme Salgado <salgado@canonical.com> | 2010-12-08 17:51:42 -0200 |
---|---|---|
committer | Guilherme Salgado <salgado@canonical.com> | 2010-12-08 17:51:42 -0200 |
commit | 79235ec6c2336337e057c582b43ee240bd668773 (patch) | |
tree | ca0fbea9013abd891394967a31fb3a4b48c811fc /hwpack | |
parent | 353921eb1fea138c7bab7936312f8ebe82cd57e5 (diff) | |
parent | 13267f9eb87ea3ce9f266063a778389cc64d6442 (diff) |
merge trunk
Diffstat (limited to 'hwpack')
-rw-r--r-- | hwpack/packages.py | 63 | ||||
-rw-r--r-- | hwpack/tarfile_matchers.py | 29 | ||||
-rw-r--r-- | hwpack/testing.py | 212 | ||||
-rw-r--r-- | hwpack/tests/__init__.py | 18 | ||||
-rw-r--r-- | hwpack/tests/test_builder.py | 34 | ||||
-rw-r--r-- | hwpack/tests/test_tarfile_matchers.py | 6 | ||||
-rw-r--r-- | hwpack/tests/test_testing.py | 232 |
7 files changed, 538 insertions, 56 deletions
diff --git a/hwpack/packages.py b/hwpack/packages.py index f3a6da6..485d8f9 100644 --- a/hwpack/packages.py +++ b/hwpack/packages.py @@ -332,45 +332,40 @@ class FetchedPackage(object): pkg.content = open(deb_file_path) return pkg - def __eq__(self, other): - # Note that we don't compare the contents here -- we assume that - # comparing the md5 checksum is enough (more philosophically, - # FetchedPackages are equal if they represent the same underlying - # package, even if they represent it in slightly different ways) - return (self.name == other.name - and self.version == other.version - and self.filename == other.filename - and self.size == other.size - and self.md5 == other.md5 - and self.architecture == other.architecture - and self.depends == other.depends - and self.pre_depends == other.pre_depends - and self.conflicts == other.conflicts - and self.recommends == other.recommends - and self.provides == other.provides - and self.replaces == other.replaces - and self.breaks == other.breaks - ) + # A list of attributes that are compared to determine equality. Note that + # we don't include the contents here -- we assume that comparing the md5 + # checksum is enough (more philosophically, FetchedPackages are equal if + # they represent the same underlying package, even if they represent it in + # slightly different ways) + _equality_attributes = ( + 'name', + 'version', + 'filename', + 'size', + 'md5', + 'architecture', + 'depends', + 'pre_depends', + 'conflicts', + 'recommends', + 'provides', + 'replaces', + 'breaks') + + @property + def _equality_data(self): + return tuple( + getattr(self, attr) for attr in self._equality_attributes) - def __hash__(self): - return hash(( - self.name, - self.version, - self.filename, - self.size, - self.md5, - self.architecture, - self.depends, - self.pre_depends, - self.conflicts, - self.recommends, - self.provides, - self.replaces, - self.breaks)) + def __eq__(self, other): + return self._equality_data == other._equality_data def __ne__(self, other): return not self.__eq__(other) + def __hash__(self): + return hash(self._equality_data) + def __repr__(self): has_content = self.content and "yes" or "no" return ( diff --git a/hwpack/tarfile_matchers.py b/hwpack/tarfile_matchers.py index 20bfeaf..919efd5 100644 --- a/hwpack/tarfile_matchers.py +++ b/hwpack/tarfile_matchers.py @@ -1,4 +1,4 @@ -from testtools.matchers import Matcher, Mismatch +from testtools.matchers import Annotate, Equals, Matcher, Mismatch class TarfileMissingPathMismatch(Mismatch): @@ -65,7 +65,8 @@ class TarfileHasFile(Matcher): def __init__(self, path, type=None, size=None, mtime=None, mtime_skew=None, mode=None, linkname=None, uid=None, - gid=None, uname=None, gname=None, content=None): + gid=None, uname=None, gname=None, content=None, + content_matcher=None): """Create a TarfileHasFile Matcher. :param path: the path that must be present. @@ -92,6 +93,9 @@ class TarfileHasFile(Matcher): None to not check. :param content: the content that `path` must have when extracted, or None to not check. + :param content_matcher: a matcher to match the content that `path` has + when extracted, or None to not check. You can't specify both + content_matcher and content. """ self.path = path self.type = type @@ -104,7 +108,18 @@ class TarfileHasFile(Matcher): self.gid = gid self.uname = uname self.gname = gname - self.content = content + if content is not None: + if content_matcher is not None: + raise ValueError( + "doesn't make sense to specify content and " + "content_matcher") + content_matcher = Equals(content) + if content_matcher is not None: + self.content_matcher = Annotate( + 'The content of path "%s" did not match' % path, + content_matcher) + else: + self.content_matcher = None def match(self, tarball): """Match a tarfile.TarFile against the expected values.""" @@ -125,11 +140,11 @@ class TarfileHasFile(Matcher): if abs(self.mtime - info.mtime) > mtime_skew: return TarfileWrongValueMismatch( "mtime", tarball, self.path, self.mtime, info.mtime) - if self.content is not None: + if self.content_matcher is not None: actual = tarball.extractfile(self.path).read() - if actual != self.content: - return TarfileWrongValueMismatch( - "content", tarball, self.path, self.content, actual) + content_mismatch = self.content_matcher.match(actual) + if content_mismatch: + return content_mismatch return None def __str__(self): diff --git a/hwpack/testing.py b/hwpack/testing.py index 67398fe..eeb8bb7 100644 --- a/hwpack/testing.py +++ b/hwpack/testing.py @@ -7,8 +7,10 @@ from StringIO import StringIO import tarfile import time +from debian.deb822 import Packages + from testtools import TestCase -from testtools.matchers import Matcher, Mismatch +from testtools.matchers import Annotate, Equals, Matcher, Mismatch from hwpack.better_tarfile import writeable_tarfile from hwpack.tarfile_matchers import TarfileHasFile @@ -345,10 +347,16 @@ class IsHardwarePack(Matcher): matchers.append(HardwarePackHasFile("FORMAT", content="1.0\n")) matchers.append(HardwarePackHasFile( "metadata", content=str(self.metadata))) - manifest = "" + manifest_lines = [] for package in self.packages: - manifest += "%s=%s\n" % (package.name, package.version) - matchers.append(HardwarePackHasFile("manifest", content=manifest)) + manifest_lines.append( + "%s=%s" % (package.name, package.version)) + matchers.append( + HardwarePackHasFile( + "manifest", + content_matcher=AfterPreproccessing( + str.splitlines, + MatchesSetwise(*map(Equals, manifest_lines))))) matchers.append(HardwarePackHasFile("pkgs", type=tarfile.DIRTYPE)) packages_with_content = [p for p in self.packages if p not in self.packages_without_content] @@ -358,7 +366,8 @@ class IsHardwarePack(Matcher): content=package.content.read())) matchers.append(HardwarePackHasFile( "pkgs/Packages", - content=get_packages_file(packages_with_content))) + content_matcher=MatchesAsPackagesFile( + *[MatchesPackage(p) for p in packages_with_content]))) matchers.append(HardwarePackHasFile( "sources.list.d", type=tarfile.DIRTYPE)) for source_id, sources_entry in self.sources.items(): @@ -373,3 +382,196 @@ class IsHardwarePack(Matcher): def __str__(self): return "Is a valid hardware pack." + + +class EachOf(object): + """Matches if each matcher matches the corresponding value. + + More easily explained by example than in words: + + >>> EachOf([Equals(1)]).match([1]) + >>> EachOf([Equals(1), Equals(2)]).match([1, 2]) + >>> EachOf([Equals(1), Equals(2)]).match([2, 1]) #doctest: +ELLIPSIS + <...MismatchesAll...> + """ + + def __init__(self, matchers): + self.matchers = matchers + + def match(self, values): + mismatches = [] + length_mismatch = Annotate( + "Length mismatch", Equals(len(self.matchers))).match(len(values)) + if length_mismatch: + mismatches.append(length_mismatch) + for matcher, value in zip(self.matchers, values): + mismatch = matcher.match(value) + if mismatch: + mismatches.append(mismatch) + if mismatches: + return MismatchesAll(mismatches) + + +class MatchesStructure(object): + """Matcher that matches an object structurally. + + 'Structurally' here means that attributes of the object being matched are + compared against given matchers. + + `fromExample` allows the creation of a matcher from a prototype object and + then modified versions can be created with `update`. + """ + + def __init__(self, **kwargs): + self.kws = kwargs + + @classmethod + def fromExample(cls, example, *attributes): + kwargs = {} + for attr in attributes: + kwargs[attr] = Equals(getattr(example, attr)) + return cls(**kwargs) + + def update(self, **kws): + new_kws = self.kws.copy() + for attr, matcher in kws.iteritems(): + if matcher is None: + new_kws.pop(attr, None) + else: + new_kws[attr] = matcher + return type(self)(**new_kws) + + def match(self, value): + matchers = [] + values = [] + for attr, matcher in self.kws.iteritems(): + matchers.append(Annotate(attr, matcher)) + values.append(getattr(value, attr)) + return EachOf(matchers).match(values) + + +def MatchesPackage(example): + """Create a `MatchesStructure` object from a `FetchedPackage`.""" + return MatchesStructure.fromExample( + example, *example._equality_attributes) + + +class MatchesSetwise(object): + """Matches if all the matchers match elements of the value being matched. + + The difference compared to `EachOf` is that the order of the matchings + does not matter. + """ + + def __init__(self, *matchers): + self.matchers = matchers + + def match(self, observed): + remaining_matchers = set(self.matchers) + not_matched = [] + for value in observed: + for matcher in remaining_matchers: + if matcher.match(value) is None: + remaining_matchers.remove(matcher) + break + else: + not_matched.append(value) + if not_matched or remaining_matchers: + remaining_matchers = list(remaining_matchers) + # There are various cases that all should be reported somewhat + # differently. + + # There are two trivial cases: + # 1) There are just some matchers left over. + # 2) There are just some values left over. + + # Then there are three more interesting cases: + # 3) There are the same number of matchers and values left over. + # 4) There are more matchers left over than values. + # 5) There are more values left over than matchers. + + if len(not_matched) == 0: + if len(remaining_matchers) > 1: + msg = "There were %s matchers left over: " % ( + len(remaining_matchers),) + else: + msg = "There was 1 matcher left over: " + msg += ', '.join(map(str, remaining_matchers)) + return Mismatch(msg) + elif len(remaining_matchers) == 0: + if len(not_matched) > 1: + return Mismatch( + "There were %s values left over: %s" % ( + len(not_matched), not_matched)) + else: + return Mismatch( + "There was 1 value left over: %s" % ( + not_matched, )) + else: + common_length = min(len(remaining_matchers), len(not_matched)) + if common_length == 0: + raise AssertionError("common_length can't be 0 here") + if common_length > 1: + msg = "There were %s mismatches" % (common_length,) + else: + msg = "There was 1 mismatch" + if len(remaining_matchers) > len(not_matched): + extra_matchers = remaining_matchers[common_length:] + msg += " and %s extra matcher" % (len(extra_matchers), ) + if len(extra_matchers) > 1: + msg += "s" + msg += ': ' + ', '.join(map(str, extra_matchers)) + elif len(not_matched) > len(remaining_matchers): + extra_values = not_matched[common_length:] + msg += " and %s extra value" % (len(extra_values), ) + if len(extra_values) > 1: + msg += "s" + msg += ': ' + str(extra_values) + return Annotate( + msg, EachOf(remaining_matchers[:common_length]) + ).match(not_matched[:common_length]) + + +def parse_packages_file_content(file_content): + packages = [] + for para in Packages.iter_paragraphs(StringIO(file_content)): + args = {} + for key, value in para.iteritems(): + key = key.lower() + if key == 'md5sum': + key = 'md5' + elif key == 'package': + key = 'name' + elif key == 'size': + value = int(value) + if key in FetchedPackage._equality_attributes: + args[key] = value + packages.append(FetchedPackage(**args)) + return packages + + +class AfterPreproccessing(object): + """Matches if the value matches after passing through a function.""" + + def __init__(self, preprocessor, matcher): + self.preprocessor = preprocessor + self.matcher = matcher + + def __str__(self): + return "AfterPreproccessing(%s, %s)" % ( + self.preprocessor, self.matcher) + + def match(self, value): + value = self.preprocessor(value) + return self.matcher.match(value) + + +def MatchesAsPackagesFile(*package_matchers): + """Matches the contents of a Packages file against the given matchers. + + The contents of the Packages file is turned into a list of FetchedPackages + using `parse_packages_file_content` above. + """ + + return AfterPreproccessing( + parse_packages_file_content, MatchesSetwise(*package_matchers)) diff --git a/hwpack/tests/__init__.py b/hwpack/tests/__init__.py index 973d9f1..11c982f 100644 --- a/hwpack/tests/__init__.py +++ b/hwpack/tests/__init__.py @@ -1,14 +1,16 @@ import unittest def test_suite(): - module_names = ['hwpack.tests.test_config', - 'hwpack.tests.test_better_tarfile', - 'hwpack.tests.test_builder', - 'hwpack.tests.test_hardwarepack', - 'hwpack.tests.test_packages', - 'hwpack.tests.test_script', - 'hwpack.tests.test_tarfile_matchers', - ] + module_names = [ + 'hwpack.tests.test_better_tarfile', + 'hwpack.tests.test_builder', + 'hwpack.tests.test_config', + 'hwpack.tests.test_hardwarepack', + 'hwpack.tests.test_packages', + 'hwpack.tests.test_script', + 'hwpack.tests.test_tarfile_matchers', + 'hwpack.tests.test_testing', + ] loader = unittest.TestLoader() suite = loader.loadTestsFromNames(module_names) return suite diff --git a/hwpack/tests/test_builder.py b/hwpack/tests/test_builder.py index 460c9d4..ecb75b4 100644 --- a/hwpack/tests/test_builder.py +++ b/hwpack/tests/test_builder.py @@ -76,6 +76,40 @@ class HardwarePackBuilderTests(TestCaseWithFixtures): metadata, [available_package], {source_id: source.sources_entry})) + def test_builds_correct_contents_multiple_packages(self): + hwpack_name = "ahwpack" + hwpack_version = "1.0" + architecture = "armel" + package_name1 = "foo" + package_name2 = "goo" + source_id = "ubuntu" + available_package1 = DummyFetchedPackage( + package_name1, "1.1", architecture=architecture) + available_package2 = DummyFetchedPackage( + package_name2, "1.2", architecture=architecture) + source = self.useFixture( + AptSourceFixture([available_package1, available_package2])) + config = self.useFixture(ConfigFileFixture( + '[hwpack]\nname=%s\npackages=%s %s\narchitectures=%s\n' + '\n[%s]\nsources-entry=%s\n' + % (hwpack_name, package_name1, package_name2, architecture, + source_id, source.sources_entry))) + builder = HardwarePackBuilder(config.filename, hwpack_version) + builder.build() + metadata = Metadata(hwpack_name, hwpack_version, architecture) + hwpack_filename = "hwpack_%s_%s_%s.tar.gz" % ( + hwpack_name, hwpack_version, architecture) + self.assertThat( + hwpack_filename, + IsHardwarePack( + metadata, [available_package1, available_package2], + {source_id: source.sources_entry})) + self.assertThat( + hwpack_filename, + IsHardwarePack( + metadata, [available_package2, available_package1], + {source_id: source.sources_entry})) + def test_obeys_include_debs(self): hwpack_name = "ahwpack" hwpack_version = "1.0" diff --git a/hwpack/tests/test_tarfile_matchers.py b/hwpack/tests/test_tarfile_matchers.py index 7a85692..aa4f3bc 100644 --- a/hwpack/tests/test_tarfile_matchers.py +++ b/hwpack/tests/test_tarfile_matchers.py @@ -168,8 +168,10 @@ class TarfileHasFileTests(TestCase): with test_tarfile(contents=[("foo", "somecontent")]) as tf: matcher = TarfileHasFile("foo", content="othercontent") mismatch = matcher.match(tf) - self.assertValueMismatch( - mismatch, tf, "foo", "content", "othercontent", "somecontent") + self.assertEquals( + "'othercontent' != 'somecontent': The content of " + "path \"foo\" did not match", + mismatch.describe()) def test_matches_mtime_with_skew(self): with test_tarfile(contents=[("foo", "")], default_mtime=12345) as tf: diff --git a/hwpack/tests/test_testing.py b/hwpack/tests/test_testing.py new file mode 100644 index 0000000..e5f1a15 --- /dev/null +++ b/hwpack/tests/test_testing.py @@ -0,0 +1,232 @@ +import doctest +import re +from StringIO import StringIO +import sys + +from testtools import TestCase +from testtools.matchers import ( + Annotate, + Equals, + Mismatch, + NotEquals, + ) +from hwpack.testing import ( + DummyFetchedPackage, + EachOf, + MatchesAsPackagesFile, + MatchesPackage, + MatchesStructure, + MatchesSetwise, + parse_packages_file_content, + ) +from hwpack.packages import ( + get_packages_file, + ) + + +def run_doctest(obj, name): + p = doctest.DocTestParser() + t = p.get_doctest( + obj.__doc__, sys.modules[obj.__module__].__dict__, name, '', 0) + r = doctest.DocTestRunner() + output = StringIO() + r.run(t, out=output.write) + return r.failures, output.getvalue() + + +class TestEachOf(TestCase): + + def test_docstring(self): + failure_count, output = run_doctest(EachOf, "EachOf") + if failure_count: + self.fail("Doctest failed with %s" % output) + + +class TestMatchesStructure(TestCase): + + class SimpleClass: + def __init__(self, x): + self.x = x + + def test_matches(self): + self.assertThat( + self.SimpleClass(1), MatchesStructure(x=Equals(1))) + + def test_mismatch(self): + self.assertRaises( + AssertionError, self.assertThat, self.SimpleClass(1), + MatchesStructure(x=NotEquals(1))) + + def test_fromExample(self): + self.assertThat( + self.SimpleClass(1), + MatchesStructure.fromExample(self.SimpleClass(1), 'x')) + + def test_update(self): + self.assertThat( + self.SimpleClass(1), + MatchesStructure(x=NotEquals(1)).update(x=Equals(1))) + + def test_update_none(self): + self.assertThat( + self.SimpleClass(1), + MatchesStructure(x=Equals(1), y=NotEquals(42)).update( + y=None)) + + +class TestMatchesPackage(TestCase): + + def test_simple(self): + observed = DummyFetchedPackage("foo", "1.1", architecture="armel") + expected = DummyFetchedPackage("foo", "1.1", architecture="armel") + self.assertThat( + observed, MatchesPackage(expected)) + + def test_mismatch(self): + observed = DummyFetchedPackage("foo", "1.1", depends="bar") + expected = DummyFetchedPackage("foo", "1.1", depends="baz") + self.assertRaises(AssertionError, self.assertThat, observed, + MatchesPackage(expected)) + + def test_skip_one_attribute(self): + observed = DummyFetchedPackage("foo", "1.1", depends="bar") + expected = DummyFetchedPackage("foo", "1.1", depends="baz") + self.assertThat( + observed, + MatchesPackage(expected).update(depends=None)) + + +class MatchesRegex(object): + + def __init__(self, pattern, flags=0): + self.pattern = pattern + self.flags = flags + def match(self, value): + if not re.match(self.pattern, value, self.flags): + return Mismatch("%r did not match %r" % (self.pattern, value)) + + +class TestMatchesSetwise(TestCase): + + def assertMismatchWithDescriptionMatching(self, value, matcher, + description_matcher): + mismatch = matcher.match(value) + if mismatch is None: + self.fail("%s matched %s" % (matcher, value)) + actual_description = mismatch.describe() + self.assertThat( + actual_description, + Annotate( + "%s matching %s" % (matcher, value), + description_matcher)) + + def test_matches(self): + self.assertIs( + None, MatchesSetwise(Equals(1), Equals(2)).match([2, 1])) + + def test_mismatches(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex('.*There was 1 mismatch$', re.S)) + + def test_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), + Equals('There was 1 matcher left over: Equals(1)')) + + def test_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [1, 2, 3], MatchesSetwise(Equals(1), Equals(2)), + Equals('There was 1 value left over: [3]')) + + def test_two_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), + MatchesRegex( + 'There were 2 matchers left over: Equals\([12]\), ' + 'Equals\([12]\)')) + + def test_two_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [1, 2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + 'There were 2 values left over: \[[34], [34]\]')) + + def test_mismatch_and_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(0), Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 1 extra matcher: Equals\([01]\)', + re.S)) + + def test_mismatch_and_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 1 extra value: \[[34]\]', + re.S)) + + def test_mismatch_and_two_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [3, 4], MatchesSetwise( + Equals(0), Equals(1), Equals(2), Equals(3)), + MatchesRegex( + '.*There was 1 mismatch and 2 extra matchers: ' + 'Equals\([012]\), Equals\([012]\)', re.S)) + + def test_mismatch_and_two_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [2, 3, 4, 5], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 2 extra values: \[[145], [145]\]', + re.S)) + + +class TestParsePackagesFileContent(TestCase): + + def test_one(self): + observed = DummyFetchedPackage("foo", "1.1") + packages_content = get_packages_file([observed]) + parsed = parse_packages_file_content(packages_content) + self.assertThat(len(parsed), Equals(1)) + self.assertThat(parsed[0], MatchesPackage(observed)) + + def test_several(self): + observed1 = DummyFetchedPackage("foo", "1.1") + observed2 = DummyFetchedPackage("bar", "1.2") + observed3 = DummyFetchedPackage("baz", "1.5") + packages_content = get_packages_file( + [observed1, observed2, observed3]) + parsed = parse_packages_file_content(packages_content) + self.assertThat(parsed, MatchesSetwise( + MatchesPackage(observed3), + MatchesPackage(observed2), + MatchesPackage(observed1))) + + +class TestMatchesAsPackagesFile(TestCase): + + def test_one(self): + observed = DummyFetchedPackage("foo", "1.1") + packages_content = get_packages_file([observed]) + self.assertThat( + packages_content, + MatchesAsPackagesFile( + MatchesPackage(observed))) + + def test_ignore_one_md5(self): + # This is what I actually care about: being able to specify that a + # packages file matches a set of packages, ignoring just a few + # details on just one package. + observed1 = DummyFetchedPackage("foo", "1.1") + observed2 = DummyFetchedPackage("bar", "1.2") + observed3 = DummyFetchedPackage("baz", "1.5") + packages_content = get_packages_file( + [observed1, observed2, observed3]) + oldmd5 = observed3.md5 + observed3._content = ''.join(reversed(observed3._content_str())) + self.assertNotEqual(oldmd5, observed3.md5) + self.assertThat(packages_content, MatchesAsPackagesFile( + MatchesPackage(observed1), + MatchesPackage(observed2), + MatchesPackage(observed3).update(md5=None))) |