manifest: Add helper methods for parsing data

Manifest data is parsed throughout Armada by traversing a dictionary
using constant keywords such as `const.KEYWORD_DATA` and
`const.KEYWORD_CHART`. Since the aforementioned dictionary is large, and
multiple Armada handlers require access to the same data, much of the
parsing logic is duplicated several times.

This change introduces a new helper class, "ManifestHelper", which
provides several methods to parse an Armada dictionary. In this commit,
additional unit tests accompany the new methods, and the methods replace
several locations where existing parsing logic is used.

Change-Id: Idebd3da5922e64f6087146ba197e2c34fdecc65d
This commit is contained in:
Drew Walters 2019-04-25 19:23:43 +00:00
parent b881e176f5
commit 521e6903dd
8 changed files with 275 additions and 122 deletions

View File

@ -20,9 +20,8 @@ from oslo_config import cfg
from armada import api
from armada.common import policy
from armada import const
from armada.handlers.lock import lock_and_thread, LockException
from armada.handlers.manifest import Manifest
from armada.handlers.manifest import ManifestHelper
from armada.handlers.test import Test
from armada.utils.release import release_prefixer
from armada.utils import validate
@ -130,40 +129,35 @@ class TestReleasesManifestController(api.BaseResource):
if not is_valid:
return
armada_obj = Manifest(
documents, target_manifest=target_manifest).get_manifest()
prefix = armada_obj[const.KEYWORD_DATA][const.KEYWORD_PREFIX]
known_releases = [release[0] for release in tiller.list_charts()]
manifest_helper = ManifestHelper(
documents, target_manifest=target_manifest)
prefix = manifest_helper.get_release_prefix()
message = {'tests': {'passed': [], 'skipped': [], 'failed': []}}
for chart in manifest_helper.get_charts():
release_name = release_prefixer(prefix, chart.get('release'))
if release_name in known_releases:
cleanup = req.get_param_as_bool('cleanup')
enable_all = req.get_param_as_bool('enable_all')
cg_test_charts = group.get('test_charts')
for group in armada_obj.get(const.KEYWORD_DATA).get(
const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS):
chart = ch['chart']
test_handler = Test(
chart,
release_name,
tiller,
cg_test_charts=cg_test_charts,
cleanup=cleanup,
enable_all=enable_all)
release_name = release_prefixer(prefix, chart.get('release'))
if release_name in known_releases:
cleanup = req.get_param_as_bool('cleanup')
enable_all = req.get_param_as_bool('enable_all')
cg_test_charts = group.get('test_charts')
if test_handler.test_enabled:
success = test_handler.test_release_for_success()
test_handler = Test(
chart,
release_name,
tiller,
cg_test_charts=cg_test_charts,
cleanup=cleanup,
enable_all=enable_all)
if test_handler.test_enabled:
success = test_handler.test_release_for_success()
if success:
message['test']['passed'].append(release_name)
else:
message['test']['failed'].append(release_name)
if success:
message['test']['passed'].append(release_name)
else:
message['test']['failed'].append(release_name)
else:
self.logger.info('Release %s not found - SKIPPING',
release_name)

View File

@ -18,10 +18,9 @@ import click
from oslo_config import cfg
from armada.cli import CliAction
from armada import const
from armada.handlers.chart_delete import ChartDelete
from armada.handlers.lock import lock_and_thread
from armada.handlers.manifest import Manifest
from armada.handlers.manifest import ManifestHelper
from armada.handlers.tiller import Tiller
from armada.utils.release import release_prefixer
@ -125,19 +124,14 @@ class DeleteChartManifest(CliAction):
with open(self.manifest) as f:
documents = list(yaml.safe_load_all(f.read()))
try:
armada_obj = Manifest(documents).get_manifest()
prefix = armada_obj.get(const.KEYWORD_DATA).get(
const.KEYWORD_PREFIX)
manifest_helper = ManifestHelper(documents)
prefix = manifest_helper.get_release_prefix()
for group in armada_obj.get(const.KEYWORD_DATA).get(
const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_DATA).get(
const.KEYWORD_CHARTS):
chart = ch.get(const.KEYWORD_DATA)
release_name = release_prefixer(
prefix, chart.get('release'))
if release_name in known_release_names:
target_deletes.append((chart, release_name))
for chart in manifest_helper.get_charts():
release_name = release_prefixer(prefix,
chart.get('release'))
if release_name in known_release_names:
target_deletes.append((chart, release_name))
except yaml.YAMLError as e:
mark = e.problem_mark
self.logger.info(

View File

@ -18,9 +18,8 @@ import click
from oslo_config import cfg
from armada.cli import CliAction
from armada import const
from armada.handlers.lock import lock_and_thread
from armada.handlers.manifest import Manifest
from armada.handlers.manifest import ManifestHelper
from armada.handlers.test import Test
from armada.handlers.tiller import Tiller
from armada.utils.release import release_prefixer
@ -144,29 +143,25 @@ class TestChartManifest(CliAction):
if self.file:
if not self.ctx.obj.get('api', False):
documents = list(yaml.safe_load_all(open(self.file).read()))
armada_obj = Manifest(
documents,
target_manifest=self.target_manifest).get_manifest()
prefix = armada_obj.get(const.KEYWORD_DATA).get(
const.KEYWORD_PREFIX)
manifest_helper = ManifestHelper(
documents, target_manifest=self.target_manifest)
for group in armada_obj.get(const.KEYWORD_DATA).get(
const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS):
chart = ch['chart']
release_prefix = manifest_helper.get_release_prefix()
for chart in manifest_helper.get_charts():
release_name = release_prefixer(release_prefix,
chart.get('release'))
release_name = release_prefixer(
prefix, chart.get('release'))
if release_name in known_release_names:
test_handler = Test(
chart,
release_name,
tiller,
cleanup=self.cleanup,
enable_all=self.enable_all)
# Test if release is deployed
if release_name in known_release_names:
test_handler = Test(
chart,
release_name,
tiller,
cleanup=self.cleanup,
enable_all=self.enable_all)
if test_handler.test_enabled:
test_handler.test_release_for_success()
if test_handler.test_enabled:
test_handler.test_release_for_success()
else:
self.logger.info('Release %s not found - SKIPPING',
release_name)

View File

@ -24,7 +24,7 @@ from armada.exceptions import source_exceptions
from armada.exceptions import tiller_exceptions
from armada.exceptions import validate_exceptions
from armada.handlers.chart_deploy import ChartDeploy
from armada.handlers.manifest import Manifest
from armada.handlers.manifest import ManifestHelper
from armada.handlers.override import Override
from armada.utils.release import release_prefixer
from armada.utils import source
@ -86,8 +86,9 @@ class Armada(object):
except (validate_exceptions.InvalidManifestException,
override_exceptions.InvalidOverrideValueException):
raise
self.manifest = Manifest(
self.documents, target_manifest=target_manifest).get_manifest()
self.manifest_helper = ManifestHelper(
self.documents, target_manifest=target_manifest)
self.manifest = self.manifest_helper.get_manifest()
self.chart_cache = {}
self.chart_deploy = ChartDeploy(
disable_update_pre, disable_update_post, self.dry_run,
@ -104,14 +105,11 @@ class Armada(object):
raise tiller_exceptions.TillerServicesUnavailableException()
# Clone the chart sources
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
for group in manifest_data.get(const.KEYWORD_GROUPS, []):
for ch in group.get(const.KEYWORD_DATA).get(
const.KEYWORD_CHARTS, []):
self.get_chart(ch)
for chart in self.manifest_helper.get_chart_documents():
self.get_chart(chart)
def get_chart(self, ch):
chart = ch.get(const.KEYWORD_DATA)
def get_chart(self, chart_document):
chart = chart_document.get(const.KEYWORD_DATA)
chart_source = chart.get('source', {})
location = chart_source.get('location')
ct_type = chart_source.get('type')
@ -159,10 +157,10 @@ class Armada(object):
self.chart_cache[source_key] = repo_dir
chart['source_dir'] = (self.chart_cache.get(source_key), subpath)
else:
name = chart['metadata']['name']
name = chart_document['metadata']['name']
raise source_exceptions.ChartSourceException(ct_type, name)
for dep in ch.get(const.KEYWORD_DATA, {}).get('dependencies', []):
for dep in chart.get('dependencies', []):
self.get_chart(dep)
def sync(self):
@ -186,12 +184,11 @@ class Armada(object):
known_releases = self.tiller.list_releases()
manifest_data = self.manifest.get(const.KEYWORD_DATA, {})
prefix = manifest_data.get(const.KEYWORD_PREFIX)
prefix = self.manifest_helper.get_release_prefix()
for cg in manifest_data.get(const.KEYWORD_GROUPS, []):
chartgroup = cg.get(const.KEYWORD_DATA)
cg_name = cg.get('metadata').get('name')
for doc in self.manifest_helper.get_chart_group_documents():
chartgroup = doc.get(const.KEYWORD_DATA)
cg_name = doc.get('metadata').get('name')
cg_desc = chartgroup.get('description', '<missing description>')
cg_sequenced = chartgroup.get('sequenced',
False) or self.force_wait
@ -259,9 +256,8 @@ class Armada(object):
self.post_flight_ops()
if self.enable_chart_cleanup:
self._chart_cleanup(
prefix,
self.manifest[const.KEYWORD_DATA][const.KEYWORD_GROUPS], msg)
chart_groups = self.manifest_helper.get_chart_group_documents()
self._chart_cleanup(prefix, chart_groups, msg)
LOG.info('Done applying manifest.')
return msg

View File

@ -136,7 +136,7 @@ class Manifest(object):
details='Could not find {} named "{}"'.format(
schema.TYPE_CHARTGROUP, name))
def build_chart_deps(self, chart):
def _build_chart_deps(self, chart):
"""Recursively build chart dependencies for ``chart``.
:param dict chart: The chart whose dependencies will be recursively
@ -153,7 +153,7 @@ class Manifest(object):
if isinstance(dep, dict):
continue
chart_dep = self.find_chart_document(dep)
self.build_chart_deps(chart_dep)
self._build_chart_deps(chart_dep)
chart[const.KEYWORD_DATA]['dependencies'][iter] = chart_dep
except Exception:
raise exceptions.ChartDependencyException(
@ -163,7 +163,7 @@ class Manifest(object):
else:
return chart
def build_chart_group(self, chart_group):
def _build_chart_group(self, chart_group):
"""Builds the chart dependencies for`charts`chart group``.
:param dict chart_group: The chart_group whose dependencies
@ -181,7 +181,7 @@ class Manifest(object):
if isinstance(chart, dict):
continue
chart_object = self.find_chart_document(chart)
self.build_chart_deps(chart_object)
self._build_chart_deps(chart_object)
chart_group[const.KEYWORD_DATA][const.KEYWORD_CHARTS][iter] = \
chart_object
except exceptions.ManifestException:
@ -192,7 +192,7 @@ class Manifest(object):
return chart_group
def build_armada_manifest(self):
def _build_armada_manifest(self):
"""Builds the Armada manifest while pulling out data
from the chart_group.
@ -207,7 +207,7 @@ class Manifest(object):
if isinstance(group, dict):
continue
chart_grp = self.find_chart_group_document(group)
self.build_chart_group(chart_grp)
self._build_chart_group(chart_grp)
self.manifest[const.KEYWORD_DATA][const.KEYWORD_GROUPS][iter] = \
chart_grp
@ -220,6 +220,73 @@ class Manifest(object):
:returns: The Armada manifest.
:rtype: dict
"""
self.build_armada_manifest()
self._build_armada_manifest()
return self.manifest
class ManifestHelper(Manifest):
def __init__(self, documents, target_manifest=None):
super(ManifestHelper, self).__init__(documents, target_manifest)
def get_chart_group_documents(self):
"""Retrieve a list of documents corresponding to the chart groups
listed in the selected/targeted manifest (self.manifest).
A chart group document contains a metadata and data section.
:returns: List of chart group documents
:rtype: list
"""
return self.get_manifest().get(const.KEYWORD_DATA).get(
const.KEYWORD_GROUPS)
def get_chart_groups(self):
"""Retrieve a list of chart group dictionaries corresponding to the
chart groups listed in the selected/targeted manifest (self.manifest).
A chart group dictionary is the data section of a chart group document.
:returns: List of chart groups dictionaries
:rtype: list
"""
group_names = self.get_chart_group_documents()
return [group.get(const.KEYWORD_DATA) for group in group_names]
def get_chart_documents(self):
"""Retrieve a list of documents corresponding to the charts listed in
all chart groups listed in the selected/targeted manifest
(self.manifest).
A chart document contains a metadata and data section.
:returns: List of chart documents
:rtype: list
"""
charts = list()
for group in self.get_chart_groups():
charts.extend(group.get(const.KEYWORD_CHARTS))
return charts
def get_charts(self):
"""Retrieve a list of chart dictionaries corresponding to the charts
listed in all chart groups listed in the selected/targeted manifest.
A chart dictionary is the data section of a chart document.
:returns: List of charts that belong to the manifest.
:rtype: list
"""
chart_documents = self.get_chart_documents()
return [chart.get(const.KEYWORD_DATA) for chart in chart_documents]
def get_release_prefix(self):
"""Retrieve the release prefix of the selected/targeted manifest.
:returns: Release prefix
:rtype: str
"""
manifest_data = self.get_manifest().get(const.KEYWORD_DATA)
return manifest_data.get(const.KEYWORD_PREFIX)

View File

@ -30,7 +30,7 @@ from armada.tests.unit.api import base
test.TestReleasesManifestController.handle.__wrapped__)
class TestReleasesManifestControllerTest(base.BaseControllerTest):
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'ManifestHelper')
@mock.patch.object(api, 'Tiller')
def test_test_controller_with_manifest(self, mock_tiller, mock_manifest):
rules = {'armada:test_manifest': '@'}
@ -127,7 +127,7 @@ class TestReleasesReleaseNameControllerTest(base.BaseControllerTest):
test.TestReleasesManifestController.handle.__wrapped__)
class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'ManifestHelper')
@mock.patch.object(api, 'Tiller')
@mock.patch.object(test.Test, 'test_release_for_success')
def test_test_controller_tiller_exc_returns_500(
@ -141,7 +141,7 @@ class TestReleasesManifestControllerNegativeTest(base.BaseControllerTest):
resp = self.app.simulate_post('/api/v1.0/tests')
self.assertEqual(500, resp.status_code)
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'ManifestHelper')
@mock.patch.object(api, 'Tiller')
def test_test_controller_validation_failure_returns_400(
self, mock_tiller, mock_manifest):

View File

@ -341,14 +341,11 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
self._test_pre_flight_ops(armada_obj)
armada_obj.post_flight_ops()
for group in armada_obj.manifest['data']['chart_groups']:
for counter, chart in enumerate(
group.get(const.KEYWORD_DATA).get(const.KEYWORD_CHARTS)):
if chart.get(
const.KEYWORD_DATA).get('source').get('type') == 'git':
mock_source.source_cleanup.assert_called_with(
CHART_SOURCES[counter][0])
charts = armada_obj.manifest_helper.get_charts()
for counter, chart in enumerate(charts):
if chart.get('source').get('type') == 'git':
mock_source.source_cleanup.assert_called_with(
CHART_SOURCES[counter][0])
# TODO(seaneagan): Separate ChartDeploy tests into separate module.
# TODO(seaneagan): Once able to make mock library sufficiently thread safe,
@ -378,8 +375,8 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
armada_obj = armada.Armada(yaml_documents, m_tiller)
armada_obj.chart_deploy.get_diff = mock.Mock()
cg = armada_obj.manifest['data']['chart_groups'][0]
chart_group = cg['data']
cg = armada_obj.manifest_helper.get_chart_group_documents()[0]
chart_group = cg.get('data')
charts = chart_group['chart_group']
cg_test_all_charts = chart_group.get('test_charts')

View File

@ -23,6 +23,71 @@ from armada.handlers import manifest
from armada.handlers import schema
from armada.utils import validate
MANIFEST = """
---
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: armada
chart_groups:
- example-group
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-group
data:
description: this is a test
sequenced: True
chart_group:
- example-chart-1
- example-chart-2
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-1
data:
chart_name: example-chart-1
release: example-chart-1
namespace: test
values: {}
source:
type: local
location: /tmp/dummy/armada
subpath: chart_1
dependencies: []
test: true
wait:
timeout: 10
upgrade:
no_hooks: false
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-2
data:
chart_name: example-chart-2
release: example-chart-2
namespace: test
values: {}
source:
type: local
location: /tmp/dummy/armada
subpath: chart_2
dependencies: []
protected:
continue_processing: false
wait:
timeout: 10
upgrade:
no_hooks: false
"""
class ManifestTestCase(testtools.TestCase):
@ -183,10 +248,10 @@ class ManifestTestCase(testtools.TestCase):
self.assertIsInstance(kis_chart, dict)
self.assertEqual(self.documents[-4], kis_chart)
def test_verify_build_armada_manifest(self):
def test_verify__build_armada_manifest(self):
armada_manifest = manifest.Manifest(self.documents)
built_armada_manifest = armada_manifest.build_armada_manifest()
built_armada_manifest = armada_manifest._build_armada_manifest()
self.assertIsNotNone(built_armada_manifest)
self.assertIsInstance(built_armada_manifest, dict)
@ -205,20 +270,20 @@ class ManifestTestCase(testtools.TestCase):
self.assertEqual(openstack_keystone_chart_group,
built_armada_manifest['data']['chart_groups'][1])
def test_verify_build_chart_group_deps(self):
def test_verify__build_chart_group_deps(self):
armada_manifest = manifest.Manifest(self.documents)
# building the deps for openstack-keystone chart group
chart_group = armada_manifest.find_chart_group_document(
'openstack-keystone')
openstack_keystone_chart_group_deps = armada_manifest. \
build_chart_group(chart_group)
_build_chart_group(chart_group)
openstack_keystone_chart_group_deps_dep_added = \
openstack_keystone_chart_group_deps[
'data']['chart_group'][0]['data']['dependencies']
# keystone chart dependencies
keystone_chart = armada_manifest.find_chart_document('keystone')
keystone_chart_with_deps = armada_manifest.build_chart_deps(
keystone_chart_with_deps = armada_manifest._build_chart_deps(
keystone_chart)
keystone_dependencies = keystone_chart_with_deps['data'][
'dependencies']
@ -230,20 +295,20 @@ class ManifestTestCase(testtools.TestCase):
chart_group = armada_manifest.find_chart_group_document(
'keystone-infra-services')
openstack_keystone_chart_group_deps = armada_manifest. \
build_chart_group(chart_group)
_build_chart_group(chart_group)
keystone_infra_services_dep_added = \
openstack_keystone_chart_group_deps[
'data']['chart_group'][0]['data']['dependencies']
# building mariadb chart dependencies
mariadb_chart = armada_manifest.find_chart_document('mariadb')
mariadb_chart_with_deps = armada_manifest.build_chart_deps(
mariadb_chart_with_deps = armada_manifest._build_chart_deps(
mariadb_chart)
mariadb_dependencies = mariadb_chart_with_deps['data']['dependencies']
# building memcached chart dependencies
memcached_chart = armada_manifest.find_chart_document('memcached')
memcached_chart_with_deps = armada_manifest.build_chart_deps(
memcached_chart_with_deps = armada_manifest._build_chart_deps(
memcached_chart)
memcached_dependencies = memcached_chart_with_deps['data'][
'dependencies']
@ -253,14 +318,14 @@ class ManifestTestCase(testtools.TestCase):
self.assertEqual(keystone_infra_services_dep_added[0],
memcached_dependencies[0])
def test_verify_build_chart_deps(self):
def test_verify__build_chart_deps(self):
armada_manifest = manifest.Manifest(self.documents)
# helm-toolkit chart
helm_toolkit_chart = armada_manifest.find_chart_document(
'helm-toolkit')
helm_toolkit_original_dependency = helm_toolkit_chart.get('data')
helm_toolkit_chart_with_deps = armada_manifest.build_chart_deps(
helm_toolkit_chart_with_deps = armada_manifest._build_chart_deps(
helm_toolkit_chart).get('data')
# since not dependent on other charts, the original and modified
@ -275,7 +340,7 @@ class ManifestTestCase(testtools.TestCase):
# keystone chart dependencies
keystone_chart = armada_manifest.find_chart_document('keystone')
original_keystone_chart = copy.deepcopy(keystone_chart)
keystone_chart_with_deps = armada_manifest.build_chart_deps(
keystone_chart_with_deps = armada_manifest._build_chart_deps(
keystone_chart)
self.assertNotEqual(original_keystone_chart, keystone_chart_with_deps)
@ -293,7 +358,7 @@ class ManifestTestCase(testtools.TestCase):
# mariadb chart dependencies
mariadb_chart = armada_manifest.find_chart_document('mariadb')
original_mariadb_chart = copy.deepcopy(mariadb_chart)
mariadb_chart_with_deps = armada_manifest.build_chart_deps(
mariadb_chart_with_deps = armada_manifest._build_chart_deps(
mariadb_chart)
self.assertNotEqual(original_mariadb_chart, mariadb_chart_with_deps)
@ -310,7 +375,7 @@ class ManifestTestCase(testtools.TestCase):
# memcached chart dependencies
memcached_chart = armada_manifest.find_chart_document('memcached')
original_memcached_chart = copy.deepcopy(memcached_chart)
memcached_chart_with_deps = armada_manifest.build_chart_deps(
memcached_chart_with_deps = armada_manifest._build_chart_deps(
memcached_chart)
self.assertNotEqual(original_memcached_chart,
@ -397,7 +462,7 @@ class ManifestNegativeTestCase(testtools.TestCase):
armada_manifest.find_chart_group_document,
'invalid')
def test_build_chart_deps_with_missing_dependency_fails(self):
def test__build_chart_deps_with_missing_dependency_fails(self):
"""Validate that attempting to build a chart that points to
a missing dependency fails.
"""
@ -405,7 +470,7 @@ class ManifestNegativeTestCase(testtools.TestCase):
valid, details = validate.validate_armada_documents(self.documents)
self.assertFalse(valid)
def test_build_chart_group_with_missing_chart_grp_fails(self):
def test__build_chart_group_with_missing_chart_grp_fails(self):
"""Validate that attempting to build a chart group document with
missing chart group fails.
"""
@ -413,10 +478,55 @@ class ManifestNegativeTestCase(testtools.TestCase):
valid, details = validate.validate_armada_documents(self.documents)
self.assertFalse(valid)
def test_build_armada_manifest_with_missing_chart_grps_fails(self):
def test__build_armada_manifest_with_missing_chart_grps_fails(self):
"""Validate that attempting to build a manifest with missing
chart groups fails.
"""
self.documents[6]['data']['chart_groups'] = ['missing-chart-groups']
valid, details = validate.validate_armada_documents(self.documents)
self.assertFalse(valid)
class ManifestHelperTestCase(testtools.TestCase):
def setUp(self):
super(ManifestHelperTestCase, self).setUp()
docs = list(yaml.safe_load_all(MANIFEST))
self.manifest_helper = manifest.ManifestHelper(docs)
def test_get_chart_group_documents(self):
chart_group_docs = self.manifest_helper.get_chart_group_documents()
self.assertIsInstance(chart_group_docs, list)
for doc in chart_group_docs:
metadata = doc.get('metadata', None)
data = doc.get('data', None)
self.assertIsInstance(metadata, dict)
self.assertIsInstance(data, dict)
def test_get_chart_groups_documents(self):
chart_groups = self.manifest_helper.get_chart_documents()
self.assertIsInstance(chart_groups, list)
for doc in chart_groups:
metadata = doc.get('metadata', None)
data = doc.get('data', None)
self.assertIsInstance(metadata, dict)
self.assertIsInstance(data, dict)
def test_get_charts(self):
charts = self.manifest_helper.get_charts()
self.assertIsInstance(charts, list)
for chart in charts:
self.assertIsInstance(chart, dict)
def test_get_chart_groups(self):
chart_groups = self.manifest_helper.get_chart_groups()
self.assertIsInstance(chart_groups, list)
for chart_group in chart_groups:
self.assertIsInstance(chart_group, dict)