summaryrefslogtreecommitdiff
path: root/linaropy/series.py
diff options
context:
space:
mode:
Diffstat (limited to 'linaropy/series.py')
-rw-r--r--linaropy/series.py550
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()