diff options
Diffstat (limited to 'linaropy/series.py')
-rw-r--r-- | linaropy/series.py | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/linaropy/series.py b/linaropy/series.py new file mode 100644 index 0000000..3db2c97 --- /dev/null +++ b/linaropy/series.py @@ -0,0 +1,550 @@ +import unittest +import logging +import os +import uuid + +from vers import Spin +from vers import Rc +from vers import Vendor +from vers import Package +from vers import packageFromStr + +from datetime import datetime +from dateutil.relativedelta import relativedelta + + +# A Series stores the instance characteristic that describes the output +class Series(object): + + series = ['candidate', 'snapshot', 'release'] + serieslongupper = ['Release-Candidate', 'Snapshot', 'Release'] + + # @ seriestype - 'type' of either 'candidate', 'snapshot', or 'release' + # @ package - String or Package object representing the series package + # name with version string, e.g., 'GCC-5.3.1' + # @ vendor - String or Vendor object representing the vendor that is + # tagged on this series. + # @ date - String or 'datetime' object representing the YYYY.MM of this + # series. It defaults to 'today' + # @ spin - Optional Spin, str, or int + # @ rc - Optional Rc, str, or int + # This will make sure snapshot doesn't have an rc and release doesn't have + # an rc for instance. + def __init__(self, seriestype, vendor=None, package=None, date=datetime.today(), spin=None, rc=None, strict=True): + if isinstance(spin, Spin): + self.spin=spin + else: + # Spin will raise an exception if spin is the wrong type. + # A None parameter will create a Spin with an internal value of None. + self.spin=Spin(spin) + + if isinstance(rc, Rc): + self.rc=rc + else: + # Rc will raise an exception if rc is the wrong type. A None parameter + # will create an Rc with an internal value of None. + self.rc=Rc(rc) + + if isinstance(date, datetime): + # Force all of the days of the month to 15 to unify comparisons. + date.replace(day=15) + self.date=date + else: + # 'date' is a string. If the input date can't be parsed by datetime + # it will throw an exception. We can't recover from it so just pass + # it up. + if len(date) < 10: + tmpdate=datetime.strptime(date, "%Y.%m") + else: + tmpdate=datetime.strptime(date, "%Y.%m.%d") + # Force all of the days of the month to 15 to unify comparisons. + self.date=tmpdate.replace(day=15) + + # If the input vendor=None just instantiate the default vendor. + if not vendor: + self.vendor=Vendor() + else: + self.vendor=vendor + + if not package: + raise TypeError('Series requires an input package.') + elif isinstance(package, Package): + self.package=package + elif isinstance(package, basestring): + self.package=packageFromStr(package) + else: + # There can't be a defaut package because it requires a version. + raise TypeError("Series 'package' unrecognized type " + str(type(package))) + + # We might want to uniquely identify a particular Series object. + self.uniqueid=str(uuid.uuid4()) + + # We store a seriestype as an integer into an enumeration array + # so that the names can be changed as desired. + try: + self.seriestype = Series.series.index(seriestype.lower()) + except ValueError: + self.seriestype = -1 # no match + raise TypeError('Invalid series type %s.' % seriestype) + + # rc can only be non-None for release-candidates. + if strict: + if self.seriestype == Series.series.index("snapshot"): + if self.rc.val != 0: + raise ValueError('A snapshot series cannot have an rc.') + elif self.seriestype == Series.series.index("release"): + if self.rc.val != 0: + raise ValueError('A release series cannot have an rc.') + + + if self.seriestype == Series.series.index("candidate") and self.rc.val is 0: + raise TypeError('A candidate series must have an rc specified.') + + # namespace/vendor-package-version-YYYY.MM-spin-rc + self.fmt = { + '%N': self.getNamespace, + '%L': self.serieslabel, + '%P': self._getPackageName, + '%p': self._getPackageNameLower, + '%V': self._getVendor, + '%v': self._getVendorLower, + '%E': self._getVersion, + '%D': self._getYYYYMM, + '%d': self.getDir, + '%S': self._getSpin, + '%R': self._getRc, + } + + def _getPackageName(self): + return self.package.pv + + def _getPackageNameLower(self): + return self.package.lower() + + def _getVendor(self): + return self.vendor.__str__() + + def _getVendorLower(self): + return self.vendor.lower() + + def _getVersion(self): + return self.package.version.strfversion("%M%m") + + def _getYYYYMM(self): + return self.date.strftime("%Y.%m") + + def _getSpin(self): + return str(self.spin).strip('-') + + def _getRc(self): + return str(self.rc).strip('-') + + def __format__(self,format_spec): + ret='' + # Iterate across the dictionary and for each key found and invoke the + # function. + # self.fmt[idx]() + import string + ret=format_spec + for key in self.fmt.iterkeys(): + # TOOD: Crap this isn't going to work because each function takes different parameters. + replac=self.fmt[key]() + ret=string.replace(ret,key,replac) + return ret + + def serieslabel(self): + # Only the 'snapshot-' is used in a label. + if self.seriestype == Series.series.index("snapshot"): + return Series.series[self.seriestype] + u'-' + else: + return u'' + + def shorttype(self): + return Series.series[self.seriestype] + + def longlowertype(self): + return Series.serieslongupper[self.seriestype].lower() + + def longuppertype(self): + return Series.serieslongupper[self.seriestype] + + def __str__(self): + return Series.series[self.seriestype] + "_" + self.uniqueid + + def label(self): + label=str(self.vendor) + u'-' + self.serieslabel() + str(self.package) + label=label + u'-' + self.date.strftime("%Y.%m") + label=label + str(self.spin) + str(self.rc) + return label + + def incrementMonth(self): + self.date = self.date + relativedelta(months=1) + + # TODO: provide a format function which can change the separator and + # capitalization, etc. + + def getDir(self): + dirstr=self.package.version.strfversion("%M%m") + dirstr=dirstr + u'-' + self.date.strftime("%Y.%m") + dirstr=dirstr + str(self.spin) + dirstr=dirstr + str(self.rc) + return dirstr + + def getNamespace(self): + namespace=u'releases' + if self.seriestype == Series.series.index("snapshot"): + namespace=u'snapshots' + return namespace + + # TODO: Document what a conformant branch name looks like. + # Return a conformant branch name from the Series information. + def getBranchname(self): + branchname=u'' + if self.seriestype == Series.series.index("snapshot"): + branchname=branchname + u'snapshots/' + else: + branchname=branchname + u'releases/' + + branchname=branchname + self.vendor.lower() + branchname=branchname + u'-' + self.package.version.strfversion("%M%m") + branchname=branchname + u'-' + self.date.strftime("%Y.%m") + branchname=branchname + str(self.spin) + branchname=branchname + str(self.rc) + + return branchname + +# Helper function which creates a Series from a properly formed branch name +# input string. +def seriesFromBranchname(branch=None): + if not isinstance(branch, basestring): + raise TypeError('seriesFromBranchname requires a basestring as input') + + if not branch: + raise ValueError('seriesFromBranchname requires a non-empty string as input') + + # Get the part before the '/'. That's the namespace + try: + namespace=branch.rsplit('/', 1)[0] + except IndexError: + raise RuntimeError('string must be <namespace>/<everything_else>') + + # Get everything after the '/'. + try: + seriesstr=branch.rsplit('/', 1)[1] + except IndexError: + raise RuntimeError('string must be <package_name>-<package_version>') + + if namespace == u'': + raise TypeError("Couldn't parse a namespace from input string") + if seriesstr == u'': + raise TypeError("Couldn't parse a series from input string") + + keys=['vendor', 'version', 'date', 'spin', 'rc'] + values=seriesstr.split('-') + dictionary=dict(zip(keys, values)) + + # if there is no spin but there is an 'rcX' in the spin key:value pair + # it means that there's really no spin but should be in the rc key:value + # pair. + if dictionary.has_key("spin") and "rc" in dictionary["spin"]: + dictionary["rc"]=dictionary["spin"] + dictionary.pop("spin", None) + + # We need to have None fields in the missing keys for when we call the + # Series constructor. + if not dictionary.has_key("rc"): + dictionary["rc"]=None + else: + # strip the "rc" and just leave the int. + if dictionary["rc"].startswith("rc"): + dictionary["rc"]=dictionary["rc"][2:] + + # We need to have None fields in the missing keys for when we call the + # Series constructor. + if not dictionary.has_key("spin"): + dictionary["spin"]=None + + seriesdate=datetime.today + if dictionary["date"]: + datekeys=['year', 'month', 'day'] + datevalues=dictionary["date"].split('.') + datefields=dict(zip(datekeys, datevalues)) + if not datefields.has_key("day"): + datefields["day"]="15" + seriesdate=datetime(int(datefields["year"]), int(datefields["month"]), int(datefields["day"])) + + if "snapshots" in namespace and dictionary["rc"]: + raise RuntimeError('A snapshots namespace can not have an rc. This is a non-conforming input.') + elif "releases" not in namespace and dictionary["rc"]: + raise RuntimeError('An rc must have a "releases" namespace. This is a non-conforming input.') + elif dictionary["rc"]: + seriesname="candidate" + elif "snapshots" in namespace: + seriesname="snapshot" + elif "releases" in namespace: + seriesname="release" + elif "snapshot" in namespace: + raise RuntimeError('"snapshot" is not a complete namespace. A conforming namespace is "snapshots".') + else: + # TODO test for unknown namespace. + raise RuntimeError('Unknown namespace in input string.') + + package=Package(package="GCC", version=dictionary["version"]) + series=Series(seriesname, package=package, date=seriesdate, spin=dictionary["spin"], rc=dictionary["rc"], strict=True) + + return series + +from vers import versionFromStr + +class TestSeries(unittest.TestCase): + + def test_missing_seriestype(self): + with self.assertRaises(TypeError): + spin=Spin() + rc=Rc() + candidate=Series(package="GCC-5.3.1", spin=spin, rc=rc) + + def test_no_match(self): + with self.assertRaises(TypeError): + candidate=Series("foobar", package="GCC-5.3.1") + + def test_partial_seriestype_match(self): + with self.assertRaises(TypeError): + candidate=Series("candid", package="GCC-5.3.1") + + def test_excessive_seriestype_match(self): + with self.assertRaises(TypeError): + candidate=Series("candidate2", package="GCC-5.3.1") + + def test_match_release(self): + release=Series("release", package="GCC-5.3.1") + self.assertEqual(str(release).split("_")[0], "release") + + def test_match_candidate_wrongcase(self): + candidate=Series("Candidate", package="GCC-5.3.1",rc="1") + self.assertEqual(str(candidate).split("_")[0], "candidate") + + def test_match_snapshot_wrongcase(self): + snapshot=Series("SNAPSHOT", package="GCC-5.3.1") + self.assertEqual(str(snapshot).split("_")[0], "snapshot") + + def test_longlowertype_candidate(self): + candidate=Series("candidate", package="GCC-5.3.1", rc="1") + self.assertEqual(candidate.longlowertype(), "release-candidate") + + def test_longuppertype_candidate(self): + candidate=Series("candidate", package="GCC-5.3.1", rc="4") + self.assertEqual(candidate.longuppertype(), "Release-Candidate") + + def test_shorttype_candidate(self): + candidate=Series("candidate", package="GCC-5.3.1", rc="1") + self.assertEqual(candidate.shorttype(), "candidate") + + def test_serieslabel_candidate(self): + candidate=Series("candidate", package="GCC-5.3.1", rc="1") + self.assertEqual(candidate.serieslabel(), "") + + def test_serieslabel_release(self): + candidate=Series("release", package="GCC-5.3.1") + self.assertEqual(candidate.serieslabel(), "") + + def test_serieslabel_snapshot(self): + candidate=Series("snapshot", package="GCC-5.3.1") + self.assertEqual(candidate.serieslabel(), "snapshot-") + + def test_empty_rc(self): + rc=Rc(7) + candidate=Series("candidate", package="GCC-5.3.1", rc=rc) + self.assertEqual(candidate.rc.vers, 7) + self.assertEqual(str(candidate.rc), "FOO") + + def test_empty_rc(self): + rc=Rc() + release=Series("release", package="GCC-5.3.1", rc=rc) + self.assertEqual(str(release.rc), "") + + release2=Series("release", package="GCC-5.3.1") + self.assertEqual(str(release2.rc), "") + + def test_specified_rc(self): + rc=Rc(7) + candidate=Series("candidate", package="GCC-5.3.1", rc=rc) + self.assertEqual(candidate.rc.val, 7) + self.assertEqual(str(candidate.rc), "-rc7") + + def test_candidate_with_no_rc(self): + with self.assertRaises(TypeError): + candidate=Series("candidate", package="GCC-5.3.1") + + with self.assertRaises(TypeError): + rc=Rc() + candidate2=Series("candidate", package="GCC-5.3.1", rc=rc) + + def test_rc_as_string(self): + candidate=Series("candidate", package="GCC-5.3.1", rc="7") + self.assertEqual(candidate.rc.val, 7) + self.assertEqual(str(candidate.rc), "-rc7") + + def test_rc_as_int(self): + candidate=Series("candidate", package="GCC-5.3.1", rc=7) + self.assertEqual(candidate.rc.val, 7) + self.assertEqual(str(candidate.rc), "-rc7") + + def test_rc_as_typeerror(self): + floatrc=7.0 + with self.assertRaises(TypeError): + snapshot=Series("snapshot", package="GCC-5.3.1", rc=floatrc) + + def test_rc_as_negative(self): + with self.assertRaises(ValueError): + snapshot=Series("snapshot", package="GCC-5.3.1", rc="-1") + + def test_missing_spin(self): + snapshot=Series("snapshot", package="GCC-5.3.1") + self.assertEqual(snapshot.spin.val, 0) + self.assertEqual(str(snapshot.spin), "") + + def test_specified_spin(self): + spin=Spin(7) + snapshot=Series("snapshot", package="GCC-5.3.1", spin=spin) + self.assertEqual(snapshot.spin.val, 7) + self.assertEqual(str(snapshot.spin), "-7") + + def test_empty_spin(self): + spin=Spin() + snapshot=Series("snapshot", package="GCC-5.3.1", spin=spin) + self.assertEqual(str(snapshot.spin), "") + + def test_spin_as_string(self): + snapshot=Series("snapshot", spin="7", package="GCC-5.3.1") + self.assertEqual(snapshot.spin.val, 7) + self.assertEqual(str(snapshot.spin), "-7") + + def test_spin_as_int(self): + snapshot=Series("snapshot", spin=7, package="GCC-5.3.1") + self.assertEqual(snapshot.spin.val, 7) + self.assertEqual(str(snapshot.spin), "-7") + + def test_spin_as_typeerror(self): + floatspin=7.0 + with self.assertRaises(TypeError): + snapshot=Series("snapshot", spin=floatspin, package="GCC-5.3.1") + + def test_spin_as_negative(self): + with self.assertRaises(ValueError): + snapshot=Series("snapshot", spin="-1", package="GCC-5.3.1") + + def test_empty_spin_and_rc(self): + release=Series("release", package="GCC-5.3.1") + self.assertEqual(release.spin.val, 0) + self.assertEqual(release.rc.val, 0) + self.assertEqual(str(release.spin), "") + self.assertEqual(str(release.rc), "") + + def test_package_as_Package(self): + package = Package("GCC", "5.3.1") + release=Series("release", package) + self.assertEqual(str(release.package), "GCC-5.3.1") + + def test_package_as_Package(self): + # Create a Version instead of a package + package = versionFromStr("5.3.1") + with self.assertRaises(TypeError): + candidate=Series("candidate", package) + + def test_package_as_None(self): + package=None + with self.assertRaises(TypeError): + candidate=Series("candidate", package) + + def test_getbranchname(self): + candidate=Series("candidate", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1") + self.assertEqual(candidate.getBranchname(), "releases/linaro-5.3-2016.05-1-rc1") + release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc=None) + self.assertEqual(release.getBranchname(), "releases/linaro-5.3-2016.05-1") + + def test_date_string(self): + candidate=Series("candidate", package="GCC-5.3.1", date="2016.05.27", spin="1", rc="1") + self.assertEqual(datetime(2016,05,15),candidate.date) + + candidate2=Series("candidate", package="GCC-5.3.1", date="2016.05", spin="1", rc="1") + self.assertEqual(datetime(2016,05,15),candidate2.date) + + with self.assertRaises(TypeError): + candidate3=Series("candidate", package="GCC-5.3.1", date=datetime("20161034234"), spin="1", rc="1") + + def test_series_strict_true(self): + with self.assertRaises(ValueError): + snapshot=Series("snapshot", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1", strict=True) + + with self.assertRaises(ValueError): + snapshot=Series("snapshot", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1") + + with self.assertRaises(ValueError): + release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1", strict=True) + + with self.assertRaises(ValueError): + release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1") + + def test_series_strict_false(self): + snapshot=Series("snapshot", package="GCC-5.3.1", date=datetime(2016,05,15), spin="6", rc="1", strict=False) + self.assertEqual(snapshot.getBranchname(), "snapshots/linaro-5.3-2016.05-6-rc1") + + release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), spin="1", rc="1", strict=False) + self.assertEqual(release.getBranchname(), "releases/linaro-5.3-2016.05-1-rc1") + + release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), rc="1", strict=False) + self.assertEqual(release.getBranchname(), "releases/linaro-5.3-2016.05-rc1") + + # TODO: Test combinations of branch names with and without spins/rcs. + + # TODO: Test whether seriesFromBranchname can detect non-conforming branch names. + + # These tests will verify that a branchname can be read, a series created, + # and then the same branch name recreated. + def test_seriesFromBranchname(self): + branch=u'snapshots/linaro-5.3-2016.05-6-rc1' + with self.assertRaises(RuntimeError): + series=seriesFromBranchname(branch=branch) + + branch2=u'snapshots/linaro-5.3-2016.05-6' + series2=seriesFromBranchname(branch=branch2) + self.assertEqual(series2.getBranchname(),u'snapshots/linaro-5.3-2016.05-6') + + branch3=u'snapshots/linaro-5.3-2016.05' + series3=seriesFromBranchname(branch=branch3) + self.assertEqual(series3.getBranchname(),u'snapshots/linaro-5.3-2016.05') + + branch4=u'releases/linaro-5.3-2016.05-6-rc1' + series4=seriesFromBranchname(branch=branch4) + self.assertEqual(series4.getBranchname(),u'releases/linaro-5.3-2016.05-6-rc1') + + branch5=u'releases/linaro-5.3-2016.05-rc1' + series5=seriesFromBranchname(branch=branch5) + self.assertEqual(series5.getBranchname(),u'releases/linaro-5.3-2016.05-rc1') + + branch6=u'releases/linaro-5.3-2016.05-6' + series6=seriesFromBranchname(branch=branch6) + self.assertEqual(series6.getBranchname(),u'releases/linaro-5.3-2016.05-6') + + branch7=u'releases/linaro-5.3-2016.05' + series7=seriesFromBranchname(branch=branch7) + self.assertEqual(series7.getBranchname(),u'releases/linaro-5.3-2016.05') + + branch8=u'snapshots/linaro-5.3-2016.05-6-rc1' + with self.assertRaises(RuntimeError): + series8=seriesFromBranchname(branch=branch8) + + branch9=u'snapshot/linaro-5.3-2016.05-6-rc1' + with self.assertRaises(RuntimeError): + series9=seriesFromBranchname(branch=branch9) + + branch10=u'snapshot/linaro-5.3-2016.05-6' + with self.assertRaises(RuntimeError): + series10=seriesFromBranchname(branch=branch10) + + # TODO: Test series.label (as there was a runtime bug) + +if __name__ == '__main__': + #logging.basicConfig(level="INFO") + unittest.main() |