d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#!/usr/bin/python
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn# CDDL HEADER START
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# The contents of this file are subject to the terms of the
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# Common Development and Distribution License (the "License").
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# You may not use this file except in compliance with the License.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# or http://www.opensolaris.org/os/licensing.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# See the License for the specific language governing permissions
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# and limitations under the License.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# When distributing Covered Code, include this CDDL HEADER in each
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# If applicable, add the following below this CDDL HEADER, with the
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# fields enclosed by brackets "[]" replaced with your own identifying
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# information: Portions Copyright [yyyy] [name of copyright owner]
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# CDDL HEADER END
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn#
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynfrom . import testutils
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynif __name__ == "__main__":
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn testutils.setup_environment("../../../proto")
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport pkg5unittest
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport os
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport pkg.config as cfg
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallynimport pkg.fmri as fmri
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport pkg.misc as misc
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport pkg.server.repository as repo
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport tempfile
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport time
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallynimport unittest
4759162d078d86628956cae4846c6efccf548e67Serge Hallynimport zlib
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
4759162d078d86628956cae4846c6efccf548e67Serge Hallynfrom six.moves.urllib.parse import urlparse
4759162d078d86628956cae4846c6efccf548e67Serge Hallynfrom six.moves.urllib.request import url2pathname
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
b08ee0ce9a17b575a35eb90778bcf166ecf3634aSerge Hallynclass TestUtilMerge(pkg5unittest.ManyDepotTestCase):
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn # Cleanup after every test.
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn persistent_setup = False
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn scheme10 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open pkg:/scheme@1.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn close
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn tree10 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open tree@1.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn close
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn amber10 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open amber@1.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add depend fmri=pkg:/tree@1.0 type=require
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn close
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn amber20 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open amber@2.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add depend fmri=pkg:/tree@1.0 type=require
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn close
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn bronze10 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open bronze@1.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add dir mode=0755 owner=root group=bin path=/usr
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add dir mode=0755 owner=root group=bin path=/usr/bin
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/sh mode=0555 owner=root group=bin path=/usr/bin/sh
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add link path=/usr/bin/jsh target=./sh
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add hardlink path=/lib/libc.bronze target=/lib/libc.so.1
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/bronze1 mode=0444 owner=root group=bin path=/etc/bronze1
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/bronze2 mode=0444 owner=root group=bin path=/etc/bronze2
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/bronzeA1 mode=0444 owner=root group=bin path=/A/B/C/D/E/F/bronzeA1
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add depend fmri=pkg:/amber@1.0 type=require
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add license tmp/copyright2 license=copyright
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn close
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn bronze20 = """
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn open bronze@2.0,5.11-0
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add dir mode=0755 owner=root group=bin path=/etc
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add dir mode=0755 owner=root group=bin path=/lib
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/sh mode=0555 owner=root group=bin path=/usr/bin/sh
80a881b232b8955b85b360d4def99e6e680ff61bSerge Hallyn add file tmp/libc.so.1 mode=0555 owner=root group=bin path=/lib/libc.bronze
80a881b232b8955b85b360d4def99e6e680ff61bSerge Hallyn add link path=/usr/bin/jsh target=./sh
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add hardlink path=/lib/libc.bronze2.0.hardlink target=/lib/libc.so.1
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn add file tmp/bronze1 mode=0444 owner=root group=bin path=/etc/bronze1
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn add file tmp/bronze2 mode=0444 owner=root group=bin path=/etc/amber2
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn add license tmp/copyright3 license=copyright
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn add file tmp/bronzeA2 mode=0444 owner=root group=bin path=/A1/B2/C3/D4/E5/F6/bronzeA2
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn add depend fmri=pkg:/amber@2.0 type=require
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn close
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn """
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn misc_files = [ "tmp/bronzeA1", "tmp/bronzeA2",
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn "tmp/bronze1", "tmp/bronze2",
42ff5f0f8767114d060f5031055038a1a1c3759aSerge Hallyn "tmp/copyright2", "tmp/copyright3",
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn "tmp/libc.so.1", "tmp/sh"]
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn def setUp(self):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """ Start two depots.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn depot 1 gets foo and moo, depot 2 gets foo and bar
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn depot1 is mapped to publisher test1 (preferred)
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn depot2 is mapped to publisher test2 """
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn # This test suite needs an actual depot.
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn pkg5unittest.ManyDepotTestCase.setUp(self, ["os.org", "os.org"],
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn start_depots=True)
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn self.make_misc_files(self.misc_files)
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn # Publish a set of packages to one repository.
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn self.dpath1 = self.dcs[1].get_repodir()
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn self.durl1 = self.dcs[1].get_depot_url()
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn self.published = self.pkgsend_bulk(self.durl1, (self.amber10,
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn self.amber20, self.bronze10, self.bronze20, self.tree10,
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn self.scheme10))
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn # Ensure timestamps of all successive publications are greater.
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn time.sleep(1)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # Publish the same set to another repository (minus the tree
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # and scheme packages).
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn self.dpath2 = self.dcs[2].get_repodir()
5a0f7f3c707bf713f35382eaabc54395d2b36b12Serge Hallyn self.durl2 = self.dcs[2].get_depot_url()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn self.published += self.pkgsend_bulk(self.durl2, (self.amber10,
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn self.amber20, self.bronze10, self.bronze20))
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn self.merge_dir = tempfile.mkdtemp(dir=self.test_root)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn repo.repository_create(self.merge_dir)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn @staticmethod
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn def get_repo(uri):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn parts = urlparse(uri, "file", allow_fragments=0)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn path = url2pathname(parts[2])
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn try:
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn return repo.Repository(root=path)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn except cfg.ConfigError as e:
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn raise repo.RepositoryError(_("The specified "
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn "repository's configuration data is not "
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn "valid:\n{0}").format(e))
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn def test_0_merge(self):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn """Verify that merge functionality works as expected."""
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn pkg_names = set()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn flist = []
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn for p in self.published:
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn f = fmri.PkgFmri(p)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn pkg_names.add(f.pkg_name)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn flist.append(f)
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn self.merge([
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn "-d {0}".format(self.merge_dir),
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn "-s arch=sparc,{0}".format(self.durl1),
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn "-s arch=i386,{0}".format(self.durl2),
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn " ".join(pkg_names),
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn ])
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # Only get the newest FMRIs for each package.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn flist.sort()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn nlist = {}
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn for f in reversed(flist):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn if f.pkg_name in nlist:
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn continue
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn nlist[f.pkg_name] = f
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn nlist = nlist.values()
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn def get_expected(f):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn exp_lines = ["set name=pkg.fmri value={0}".format(f)]
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn for dc in self.dcs.values():
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn repo = dc.get_repo()
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn mpath = repo.manifest(f)
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn if not os.path.exists(mpath):
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn # Not in this repository, check next.
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn continue
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn m = open(mpath, "r")
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn for l in m:
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn if l.find("name=pkg.fmri") > -1:
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn continue
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn if l.find("name=variant") > -1:
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn continue
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn if not l.strip():
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn continue
4759162d078d86628956cae4846c6efccf548e67Serge Hallyn exp_lines.append(l.strip())
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn m.close()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn if f.pkg_name in ("tree", "scheme"):
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # These packages are only published for sparc.
3eecde703e9ac3af788ac17357f378d6b6d7c658Serge Hallyn exp_lines.append("set name=variant.arch value=sparc")
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn else:
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn # Everything else is published for all variants.
52c8f624b5f9ef665f33a7aa80e0aa18b91daa4aSerge Hallyn exp_lines.append("set name=variant.arch value=sparc value=i386")
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn return "\n".join(sorted(exp_lines))
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # Now load the manifest file for each package and verify that
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # the merged manifest matches expectations.
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn for f in nlist:
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn mpath = os.path.join(self.merge_dir, "publisher",
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn "os.org", "pkg", f.get_dir_path())
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn m = open(mpath, "r")
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn returned = "".join(sorted(l for l in m))
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn returned = returned.strip()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn m.close()
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn
d1458ac8d13880f83fa2d1e08623b97c50d311d7Serge Hallyn # Generate expected and verify.
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn expected = get_expected(f)
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn self.assertEqualDiff(expected, returned)
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallyn
427bffc7a10c9015dc78ef52543f7b8cb9414359Serge Hallynif __name__ == "__main__":
5a0f7f3c707bf713f35382eaabc54395d2b36b12Serge Hallyn unittest.main()
5a0f7f3c707bf713f35382eaabc54395d2b36b12Serge Hallyn