Merge lp:~bac/charmworldlib/check-constraints into lp:charmworldlib/0.2

Proposed by Brad Crittenden
Status: Merged
Merge reported by: Marco Ceppi
Merged at revision: not available
Proposed branch: lp:~bac/charmworldlib/check-constraints
Merge into: lp:charmworldlib/0.2
Diff against target: 652 lines (+211/-370)
7 files modified
Makefile (+5/-1)
charmworldlib/bundle.py (+1/-1)
charmworldlib/utils.py (+76/-0)
ez_setup.py (+0/-361)
setup.py (+1/-6)
tests/test_bundle.py (+4/-1)
tests/test_utils.py (+124/-0)
To merge this branch: bzr merge lp:~bac/charmworldlib/check-constraints
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+208669@code.launchpad.net

Description of the change

Add parse_constraints and check_constraints.

Also, removes the dependency on ez_setup since it caused the package to not
build and was unnecessary. There is a real system dependency on
python-virtualenv which install python-setuptools, so setuptools will always
be available and ez_setup not required.

https://codereview.appspot.com/69430043/

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Reviewers: mp+208669_code.launchpad.net,

Message:
Please take a look.

Description:
Add parse_constraints and check_constraints.

Also, removes the dependency on ez_setup since it caused the package to
not
build and was unnecessary. There is a real system dependency on
python-virtualenv which install python-setuptools, so setuptools will
always
be available and ez_setup not required.

https://code.launchpad.net/~bac/charmworldlib/check-constraints/+merge/208669

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/69430043/

Affected files (+166, -370 lines):
   M Makefile
   A [revision details]
   M charmworldlib/bundle.py
   D ez_setup.py
   M setup.py
   M tests/test_bundle.py

33. By Brad Crittenden

Move constraint processing to utils.

34. By Brad Crittenden

Add new utils files

Revision history for this message
Brad Crittenden (bac) wrote :
35. By Brad Crittenden

Use forked version number for now.

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Richard Harding (rharding) wrote :

LGTM thanks for the update and sanity checking.

https://codereview.appspot.com/69430043/diff/40001/setup.py
File setup.py (right):

https://codereview.appspot.com/69430043/diff/40001/setup.py#newcode13
setup.py:13: version="0.2.4-2-bac",
this makes constraint changes. Should it be 0.2.5-1~bac?

https://codereview.appspot.com/69430043/

36. By Brad Crittenden

Update constraint parsing to be more robust.

Revision history for this message
Brad Crittenden (bac) wrote :

Please take a look.

https://codereview.appspot.com/69430043/diff/40001/setup.py
File setup.py (right):

https://codereview.appspot.com/69430043/diff/40001/setup.py#newcode13
setup.py:13: version="0.2.4-2-bac",
On 2014/02/28 14:42:11, rharding wrote:
> this makes constraint changes. Should it be 0.2.5-1~bac?

Done.

https://codereview.appspot.com/69430043/

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-02-04 15:21:40 +0000
3+++ Makefile 2014-03-04 13:42:34 +0000
4@@ -1,3 +1,4 @@
5+SYSDEPS = python-virtualenv
6 VENVS = .venv2 .venv3
7 VENV2 = $(word 1, $(VENVS))
8 VENV3 = $(word 2, $(VENVS))
9@@ -30,6 +31,9 @@
10
11 setup: $(VENVS)
12
13+sysdeps:
14+ sudo apt-get install --yes $(SYSDEPS)
15+
16 test: setup
17 $(VENV3)/bin/nosetests -s --verbosity=2
18 $(VENV2)/bin/nosetests -s --verbosity=2 --with-coverage --cover-package=charmworldlib
19@@ -48,6 +52,6 @@
20 clean-all: clean
21 rm -rf .pip-cache
22
23-.PHONY: setup test lint clean clean-all install install_2 install_3
24+.PHONY: setup sysdeps test lint clean clean-all install install_2 install_3
25
26 .DEFAULT_GOAL := all
27
28=== modified file 'charmworldlib/bundle.py'
29--- charmworldlib/bundle.py 2014-01-16 09:35:29 +0000
30+++ charmworldlib/bundle.py 2014-03-04 13:42:34 +0000
31@@ -1,5 +1,5 @@
32-
33 import yaml
34+
35 from . import api
36
37
38
39=== added file 'charmworldlib/utils.py'
40--- charmworldlib/utils.py 1970-01-01 00:00:00 +0000
41+++ charmworldlib/utils.py 2014-03-04 13:42:34 +0000
42@@ -0,0 +1,76 @@
43+import collections
44+
45+
46+# Define a sequence of allowed constraints to be used in the process of
47+# preparing the bundle object. See the _prepare_constraints function below.
48+ALLOWED_CONSTRAINTS = (
49+ 'arch',
50+ 'container',
51+ 'cpu-cores',
52+ 'cpu-power',
53+ 'mem',
54+ 'root-disk',
55+ # XXX: BradCrittenden 2014-02-12:
56+ # tags are supported by MaaS only so they are not currently implemented.
57+ # It is unclear whether the GUI should support them or not so they are
58+ # being left out for now.
59+ # Also, tags are a comma-separated, which would clash with the currently
60+ # broken constraint parsing in the GUI.
61+ # 'tags',
62+)
63+
64+
65+def parse_constraints(original_constraints):
66+ """Parse the constraints and validate them.
67+
68+ constraints is a string of key=value pairs or a dict. Due to
69+ historical reasons, many bundles use a comma-separated list rather
70+ than the space-separated list juju-core expects. This method
71+ handles both separators.
72+
73+ Returns a dict of validated constraints.
74+ Raises ValueError if one or more constraints is invalid.
75+ """
76+
77+ constraints = original_constraints
78+ if not isinstance(constraints, collections.Mapping):
79+ constraints = constraints.strip()
80+ if not constraints:
81+ return {}
82+ #pairs = CONSTRAINTS_REGEX.findall(constraints)
83+ num_equals = constraints.count('=')
84+ # Comma separation is supported but deprecated. Attempt splitting on
85+ # it first as it yields better results if a mix of commas and spaces
86+ # is used.
87+ pairs = constraints.split(',')
88+ if num_equals != len(pairs):
89+ pairs = constraints.split(' ')
90+ if num_equals != len(pairs):
91+ raise ValueError('invalid constraints: {}'.format(
92+ original_constraints))
93+
94+ constraints = {}
95+ for item in pairs:
96+ k, v = item.split('=')
97+ if v.find(',') != -1:
98+ raise ValueError('invalid constraints: {}'.format(
99+ original_constraints))
100+
101+ constraints[k.strip()] = v.strip()
102+
103+ unsupported = set(constraints).difference(ALLOWED_CONSTRAINTS)
104+ if unsupported:
105+ msg = 'unsupported constraints: {}'.format(
106+ ', '.join(sorted(unsupported)))
107+ raise ValueError(msg)
108+ return constraints
109+
110+
111+def check_constraints(original_constraints):
112+ """Check to see that constraints are space-separated and valid."""
113+ try:
114+ parsed = parse_constraints(original_constraints)
115+ except ValueError:
116+ return False
117+ tokens = original_constraints.strip().split()
118+ return len(parsed) == len(tokens)
119
120=== removed file 'ez_setup.py'
121--- ez_setup.py 2014-02-19 13:03:36 +0000
122+++ ez_setup.py 1970-01-01 00:00:00 +0000
123@@ -1,361 +0,0 @@
124-#!/usr/bin/env python
125-"""Bootstrap setuptools installation
126-
127-To use setuptools in your package's setup.py, include this
128-file in the same directory and add this to the top of your setup.py::
129-
130- from ez_setup import use_setuptools
131- use_setuptools()
132-
133-To require a specific version of setuptools, set a download
134-mirror, or use an alternate download directory, simply supply
135-the appropriate options to ``use_setuptools()``.
136-
137-This file can also be run as a script to install or upgrade setuptools.
138-"""
139-import os
140-import shutil
141-import sys
142-import tempfile
143-import tarfile
144-import optparse
145-import subprocess
146-import platform
147-import textwrap
148-
149-from distutils import log
150-
151-try:
152- from site import USER_SITE
153-except ImportError:
154- USER_SITE = None
155-
156-DEFAULT_VERSION = "0.9.8"
157-DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
158-
159-def _python_cmd(*args):
160- args = (sys.executable,) + args
161- return subprocess.call(args) == 0
162-
163-def _install(tarball, install_args=()):
164- # extracting the tarball
165- tmpdir = tempfile.mkdtemp()
166- log.warn('Extracting in %s', tmpdir)
167- old_wd = os.getcwd()
168- try:
169- os.chdir(tmpdir)
170- tar = tarfile.open(tarball)
171- _extractall(tar)
172- tar.close()
173-
174- # going in the directory
175- subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
176- os.chdir(subdir)
177- log.warn('Now working in %s', subdir)
178-
179- # installing
180- log.warn('Installing Setuptools')
181- if not _python_cmd('setup.py', 'install', *install_args):
182- log.warn('Something went wrong during the installation.')
183- log.warn('See the error message above.')
184- # exitcode will be 2
185- return 2
186- finally:
187- os.chdir(old_wd)
188- shutil.rmtree(tmpdir)
189-
190-
191-def _build_egg(egg, tarball, to_dir):
192- # extracting the tarball
193- tmpdir = tempfile.mkdtemp()
194- log.warn('Extracting in %s', tmpdir)
195- old_wd = os.getcwd()
196- try:
197- os.chdir(tmpdir)
198- tar = tarfile.open(tarball)
199- _extractall(tar)
200- tar.close()
201-
202- # going in the directory
203- subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
204- os.chdir(subdir)
205- log.warn('Now working in %s', subdir)
206-
207- # building an egg
208- log.warn('Building a Setuptools egg in %s', to_dir)
209- _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
210-
211- finally:
212- os.chdir(old_wd)
213- shutil.rmtree(tmpdir)
214- # returning the result
215- log.warn(egg)
216- if not os.path.exists(egg):
217- raise IOError('Could not build the egg.')
218-
219-
220-def _do_download(version, download_base, to_dir, download_delay):
221- egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
222- % (version, sys.version_info[0], sys.version_info[1]))
223- if not os.path.exists(egg):
224- tarball = download_setuptools(version, download_base,
225- to_dir, download_delay)
226- _build_egg(egg, tarball, to_dir)
227- sys.path.insert(0, egg)
228-
229- # Remove previously-imported pkg_resources if present (see
230- # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
231- if 'pkg_resources' in sys.modules:
232- del sys.modules['pkg_resources']
233-
234- import setuptools
235- setuptools.bootstrap_install_from = egg
236-
237-
238-def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
239- to_dir=os.curdir, download_delay=15):
240- to_dir = os.path.abspath(to_dir)
241- rep_modules = 'pkg_resources', 'setuptools'
242- imported = set(sys.modules).intersection(rep_modules)
243- try:
244- import pkg_resources
245- except ImportError:
246- return _do_download(version, download_base, to_dir, download_delay)
247- try:
248- pkg_resources.require("setuptools>=" + version)
249- return
250- except pkg_resources.DistributionNotFound:
251- return _do_download(version, download_base, to_dir, download_delay)
252- except pkg_resources.VersionConflict as VC_err:
253- if imported:
254- msg = textwrap.dedent("""
255- The required version of setuptools (>={version}) is not available,
256- and can't be installed while this script is running. Please
257- install a more recent version first, using
258- 'easy_install -U setuptools'.
259-
260- (Currently using {VC_err.args[0]!r})
261- """).format(VC_err=VC_err, version=version)
262- sys.stderr.write(msg)
263- sys.exit(2)
264-
265- # otherwise, reload ok
266- del pkg_resources, sys.modules['pkg_resources']
267- return _do_download(version, download_base, to_dir, download_delay)
268-
269-def _clean_check(cmd, target):
270- """
271- Run the command to download target. If the command fails, clean up before
272- re-raising the error.
273- """
274- try:
275- subprocess.check_call(cmd)
276- except subprocess.CalledProcessError:
277- if os.access(target, os.F_OK):
278- os.unlink(target)
279- raise
280-
281-def download_file_powershell(url, target):
282- """
283- Download the file at url to target using Powershell (which will validate
284- trust). Raise an exception if the command cannot complete.
285- """
286- target = os.path.abspath(target)
287- cmd = [
288- 'powershell',
289- '-Command',
290- "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
291- ]
292- _clean_check(cmd, target)
293-
294-def has_powershell():
295- if platform.system() != 'Windows':
296- return False
297- cmd = ['powershell', '-Command', 'echo test']
298- devnull = open(os.path.devnull, 'wb')
299- try:
300- try:
301- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
302- except:
303- return False
304- finally:
305- devnull.close()
306- return True
307-
308-download_file_powershell.viable = has_powershell
309-
310-def download_file_curl(url, target):
311- cmd = ['curl', url, '--silent', '--output', target]
312- _clean_check(cmd, target)
313-
314-def has_curl():
315- cmd = ['curl', '--version']
316- devnull = open(os.path.devnull, 'wb')
317- try:
318- try:
319- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
320- except:
321- return False
322- finally:
323- devnull.close()
324- return True
325-
326-download_file_curl.viable = has_curl
327-
328-def download_file_wget(url, target):
329- cmd = ['wget', url, '--quiet', '--output-document', target]
330- _clean_check(cmd, target)
331-
332-def has_wget():
333- cmd = ['wget', '--version']
334- devnull = open(os.path.devnull, 'wb')
335- try:
336- try:
337- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
338- except:
339- return False
340- finally:
341- devnull.close()
342- return True
343-
344-download_file_wget.viable = has_wget
345-
346-def download_file_insecure(url, target):
347- """
348- Use Python to download the file, even though it cannot authenticate the
349- connection.
350- """
351- try:
352- from urllib.request import urlopen
353- except ImportError:
354- from urllib2 import urlopen
355- src = dst = None
356- try:
357- src = urlopen(url)
358- # Read/write all in one block, so we don't create a corrupt file
359- # if the download is interrupted.
360- data = src.read()
361- dst = open(target, "wb")
362- dst.write(data)
363- finally:
364- if src:
365- src.close()
366- if dst:
367- dst.close()
368-
369-download_file_insecure.viable = lambda: True
370-
371-def get_best_downloader():
372- downloaders = [
373- download_file_powershell,
374- download_file_curl,
375- download_file_wget,
376- download_file_insecure,
377- ]
378-
379- for dl in downloaders:
380- if dl.viable():
381- return dl
382-
383-def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
384- to_dir=os.curdir, delay=15,
385- downloader_factory=get_best_downloader):
386- """Download setuptools from a specified location and return its filename
387-
388- `version` should be a valid setuptools version number that is available
389- as an egg for download under the `download_base` URL (which should end
390- with a '/'). `to_dir` is the directory where the egg will be downloaded.
391- `delay` is the number of seconds to pause before an actual download
392- attempt.
393-
394- ``downloader_factory`` should be a function taking no arguments and
395- returning a function for downloading a URL to a target.
396- """
397- # making sure we use the absolute path
398- to_dir = os.path.abspath(to_dir)
399- tgz_name = "setuptools-%s.tar.gz" % version
400- url = download_base + tgz_name
401- saveto = os.path.join(to_dir, tgz_name)
402- if not os.path.exists(saveto): # Avoid repeated downloads
403- log.warn("Downloading %s", url)
404- downloader = downloader_factory()
405- downloader(url, saveto)
406- return os.path.realpath(saveto)
407-
408-
409-def _extractall(self, path=".", members=None):
410- """Extract all members from the archive to the current working
411- directory and set owner, modification time and permissions on
412- directories afterwards. `path' specifies a different directory
413- to extract to. `members' is optional and must be a subset of the
414- list returned by getmembers().
415- """
416- import copy
417- import operator
418- from tarfile import ExtractError
419- directories = []
420-
421- if members is None:
422- members = self
423-
424- for tarinfo in members:
425- if tarinfo.isdir():
426- # Extract directories with a safe mode.
427- directories.append(tarinfo)
428- tarinfo = copy.copy(tarinfo)
429- tarinfo.mode = 448 # decimal for oct 0700
430- self.extract(tarinfo, path)
431-
432- # Reverse sort directories.
433- directories.sort(key=operator.attrgetter('name'), reverse=True)
434-
435- # Set correct owner, mtime and filemode on directories.
436- for tarinfo in directories:
437- dirpath = os.path.join(path, tarinfo.name)
438- try:
439- self.chown(tarinfo, dirpath)
440- self.utime(tarinfo, dirpath)
441- self.chmod(tarinfo, dirpath)
442- except ExtractError as e:
443- if self.errorlevel > 1:
444- raise
445- else:
446- self._dbg(1, "tarfile: %s" % e)
447-
448-
449-def _build_install_args(options):
450- """
451- Build the arguments to 'python setup.py install' on the setuptools package
452- """
453- return ['--user'] if options.user_install else []
454-
455-def _parse_args():
456- """
457- Parse the command line for options
458- """
459- parser = optparse.OptionParser()
460- parser.add_option(
461- '--user', dest='user_install', action='store_true', default=False,
462- help='install in user site package (requires Python 2.6 or later)')
463- parser.add_option(
464- '--download-base', dest='download_base', metavar="URL",
465- default=DEFAULT_URL,
466- help='alternative URL from where to download the setuptools package')
467- parser.add_option(
468- '--insecure', dest='downloader_factory', action='store_const',
469- const=lambda: download_file_insecure, default=get_best_downloader,
470- help='Use internal, non-validating downloader'
471- )
472- options, args = parser.parse_args()
473- # positional arguments are ignored
474- return options
475-
476-def main(version=DEFAULT_VERSION):
477- """Install or upgrade setuptools and EasyInstall"""
478- options = _parse_args()
479- tarball = download_setuptools(download_base=options.download_base,
480- downloader_factory=options.downloader_factory)
481- return _install(tarball, _build_install_args(options))
482-
483-if __name__ == '__main__':
484- sys.exit(main())
485
486=== modified file 'setup.py'
487--- setup.py 2014-02-19 13:04:04 +0000
488+++ setup.py 2014-03-04 13:42:34 +0000
489@@ -3,17 +3,12 @@
490 # Copyright 2013 Marco Ceppi. This software is licensed under the
491 # GNU General Public License version 3 (see the file LICENSE).
492
493-import ez_setup
494-
495-
496-ez_setup.use_setuptools()
497-
498 from setuptools import setup
499
500
501 setup(
502 name='charmworldlib',
503- version="0.2.4-1",
504+ version="0.2.5-1",
505 packages=['charmworldlib'],
506 maintainer='Marco Ceppi',
507 maintainer_email='marco@ceppi.net',
508
509=== modified file 'tests/test_bundle.py'
510--- tests/test_bundle.py 2014-01-16 09:35:29 +0000
511+++ tests/test_bundle.py 2014-03-04 13:42:34 +0000
512@@ -3,7 +3,10 @@
513 import unittest
514
515 from mock import patch
516-from charmworldlib.bundle import Bundles, Bundle
517+from charmworldlib.bundle import (
518+ Bundle,
519+ Bundles,
520+)
521
522
523 class BundlesTest(unittest.TestCase):
524
525=== added file 'tests/test_utils.py'
526--- tests/test_utils.py 1970-01-01 00:00:00 +0000
527+++ tests/test_utils.py 2014-03-04 13:42:34 +0000
528@@ -0,0 +1,124 @@
529+"""Unit tests for utils."""
530+
531+import unittest
532+
533+from charmworldlib.utils import (
534+ check_constraints,
535+ parse_constraints,
536+)
537+
538+
539+class TestParseConstraints(unittest.TestCase):
540+
541+ def test_valid_constraints(self):
542+ # Valid constraints are returned as they are.
543+ constraints = {
544+ 'arch': 'i386',
545+ 'cpu-cores': 4,
546+ 'cpu-power': 2,
547+ 'mem': 2000,
548+ 'root-disk': '1G',
549+ 'container': 'lxc',
550+ }
551+ self.assertEqual(constraints, parse_constraints(constraints))
552+
553+ def test_valid_constraints_subset(self):
554+ # A subset of valid constraints is returned as it is.
555+ constraints = {'cpu-cores': '4', 'cpu-power': 2}
556+ self.assertEqual(constraints, parse_constraints(constraints))
557+
558+ def test_invalid_constraints(self):
559+ # A ValueError is raised if unsupported constraints are found.
560+ with self.assertRaises(ValueError) as context_manager:
561+ parse_constraints({'arch': 'i386', 'not-valid': 'bang!'})
562+ self.assertEqual(
563+ 'unsupported constraints: not-valid',
564+ str(context_manager.exception))
565+
566+ def test_string_constraints_space_separated(self):
567+ # String constraints are converted to a dict.
568+ constraints = 'arch=i386 cpu-cores=4 cpu-power=2 mem=2000'
569+ expected = {
570+ 'arch': 'i386',
571+ 'cpu-cores': '4',
572+ 'cpu-power': '2',
573+ 'mem': '2000',
574+ }
575+ self.assertEqual(expected, parse_constraints(constraints))
576+
577+ def test_string_constraints_comma_separated(self):
578+ # String constraints are converted to a dict.
579+ constraints = 'arch=i386,cpu-cores=4,cpu-power=2,mem=2000'
580+ expected = {
581+ 'arch': 'i386',
582+ 'cpu-cores': '4',
583+ 'cpu-power': '2',
584+ 'mem': '2000',
585+ }
586+ self.assertEqual(expected, parse_constraints(constraints))
587+
588+ def test_string_constraints_mixed_separated(self):
589+ # String constraints are converted to a dict.
590+ constraints = 'arch=i386, cpu-cores=4, cpu-power=2,mem=2000'
591+ expected = {
592+ 'arch': 'i386',
593+ 'cpu-cores': '4',
594+ 'cpu-power': '2',
595+ 'mem': '2000',
596+ }
597+ self.assertEqual(expected, parse_constraints(constraints))
598+
599+ def test_unsupported_string_constraints(self):
600+ # A ValueError is raised if unsupported string constraints are found.
601+ with self.assertRaises(ValueError) as context_manager:
602+ parse_constraints('cpu-cores=4 invalid1=1 invalid2=2')
603+ self.assertEqual(
604+ 'unsupported constraints: invalid1, invalid2',
605+ str(context_manager.exception))
606+
607+ def test_invalid_string_constraints(self):
608+ # A ValueError is raised if an invalid string is passed.
609+ with self.assertRaises(ValueError) as context_manager:
610+ parse_constraints('arch=,cpu-cores=,')
611+ self.assertEqual(
612+ 'invalid constraints: arch=,cpu-cores=,',
613+ str(context_manager.exception))
614+
615+ def test_invalid_no_pairs(self):
616+ # A ValueError is raised if an invalid string is passed.
617+ with self.assertRaises(ValueError) as context_manager:
618+ parse_constraints('yo')
619+ self.assertEqual(
620+ 'invalid constraints: yo',
621+ str(context_manager.exception))
622+
623+ def test_not_key_value_pairs_constraints(self):
624+ # A ValueError is raised if a string doesn't have the same number of
625+ # tokens as '=' characters.
626+ with self.assertRaises(ValueError) as context_manager:
627+ parse_constraints('arch=1,cpu-cores')
628+ self.assertEqual(
629+ 'invalid constraints: arch=1,cpu-cores',
630+ str(context_manager.exception))
631+
632+ def test_empty_constraints_return_empty_dict(self):
633+ constraints = parse_constraints('')
634+ self.assertEqual({}, constraints)
635+
636+
637+class TestCheckConstraints(unittest.TestCase):
638+
639+ def test_space_separated(self):
640+ constraints = 'cpu-cores=4 mem=2000 root-disk=1'
641+ result = check_constraints(constraints)
642+ self.assertTrue(result)
643+
644+ def test_comma_separated(self):
645+ constraints = 'cpu-cores=4,mem=2000,root-disk=1'
646+ result = check_constraints(constraints)
647+ self.assertFalse(result)
648+
649+ def test_invalid_separated(self):
650+ constraints = 'invalid=4,mem=2000,root-disk=1'
651+ result = check_constraints(constraints)
652+ self.assertFalse(result)

Subscribers

People subscribed via source and target branches

to all changes: