Add checker.ConstraintSuite.
- A ConstraintSuite is subclassed to define constraints,
similar to how unittest.TestCase is subclassed to define
test methods.
- Right now just checks the instance for methods named
"check..." and runs them. A check method should raise
an exception if a constraint is violated. Check methods
are passed a program and project ConfigBundle.
- Will improve error reporting in the future (e.g. run all
checks instead of failing on the first one, return names
of failing checks, etc.)
BUG=chromium:1051187
TEST=python3 -m unittest discover -p *test.py
Change-Id: Ic155c9f1a2b7b442c025fde460c503f3ce9c6c50
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/config/+/2054750
Commit-Queue: Andrew Lamb <andrewlamb@chromium.org>
Tested-by: Andrew Lamb <andrewlamb@chromium.org>
Reviewed-by: David Burger <dburger@chromium.org>
diff --git a/payload_utils/checker/constraint_suite.py b/payload_utils/checker/constraint_suite.py
new file mode 100644
index 0000000..085c587
--- /dev/null
+++ b/payload_utils/checker/constraint_suite.py
@@ -0,0 +1,62 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Defines ConstraintSuite, which is subclassed to define constraints."""
+
+import inspect
+
+from bindings.src.config.api import config_bundle_pb2
+
+
+class InvalidConstraintSuiteError(Exception):
+ """Exception raised when an invalid ConstraintSuite is defined."""
+
+
+class ConstraintSuite:
+ """A class whose instances are suites of constraints.
+
+ Constraint authors should subclass ConstraintSuite and add methods starting
+ with "check" for each constraint they want to enforce. Each check method
+ should accept two ConfigBundles, with parameter names "project_config" and
+ "program_config". A check method is considered failed iff it raises an
+ Exception.
+
+ A ConstraintSuite subclass should group related constraints. For example a
+ suite to check form factor constraints could look like:
+
+ class FormFactorConstraintSuite(ConstraintSuite):
+
+ def checkFormFactorDefined(self, program_config, project_config):
+ ...
+
+ def checkFormFactorAllowedByProgram(self, program_config, project_config):
+ ...
+
+ A ConstraintSuite that defines no check methods will raise an exception on
+ initialization.
+ """
+
+ def __init__(self):
+ super().__init__()
+
+ def is_check(name, value):
+ return name.startswith('check') and inspect.ismethod(value)
+
+ self._checks = [
+ value for name, value in inspect.getmembers(self)
+ if is_check(name, value)
+ ]
+
+ if not self._checks:
+ raise InvalidConstraintSuiteError('No checks found on %s' % type(self))
+
+ def run_checks(self, program_config: config_bundle_pb2.ConfigBundle,
+ project_config: config_bundle_pb2.ConfigBundle):
+ """Runs all of the checks on an instance.
+
+ Args:
+ program_config: The program's config, to pass to each check.
+ project_config: The project's config, to pass to each check.
+ """
+ for method in self._checks:
+ method(program_config=program_config, project_config=project_config)
diff --git a/payload_utils/checker/constraint_suite_test.py b/payload_utils/checker/constraint_suite_test.py
new file mode 100644
index 0000000..1a87e0a
--- /dev/null
+++ b/payload_utils/checker/constraint_suite_test.py
@@ -0,0 +1,69 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Tests for constraint_suite."""
+
+import unittest
+
+from bindings.src.config.api.config_bundle_pb2 import ConfigBundle
+from bindings.src.config.api.design_pb2 import DesignList, Design
+from bindings.src.config.api.program_pb2 import ProgramList, Program
+
+from checker.constraint_suite import (ConstraintSuite,
+ InvalidConstraintSuiteError)
+
+
+class ValidConstraintSuite(ConstraintSuite):
+ """A valid constraint suite, that defines two checks."""
+
+ def _helper_method(self):
+ assert False, "helper_method should never be called"
+
+ def check_program_valid(self, program_config, project_config):
+ if program_config.programs.value[0].name != 'TestProgram1':
+ raise AssertionError("Program name must be 'TestProgram1'")
+
+ def check_project_valid(self, program_config, project_config):
+ if project_config.designs.value[0].name != 'TestDesign1':
+ raise AssertionError("Design name must be 'TestDesign1'")
+
+
+class InvalidConstraintSuite(ConstraintSuite):
+ """An invalid constraint suite, that defines no checks."""
+
+ def helper_method1(self):
+ pass
+
+ def helper_method2(self):
+ pass
+
+
+class ConstraintSuiteTest(unittest.TestCase):
+
+ def test_runs_checks(self):
+ """Tests running checks on a project that fulfills constraints."""
+ program_config = ConfigBundle(
+ programs=ProgramList(value=[Program(name='TestProgram1')]))
+ project_config = ConfigBundle(
+ designs=DesignList(value=[Design(name='TestDesign1')]))
+
+ ValidConstraintSuite().run_checks(
+ program_config=program_config, project_config=project_config)
+
+ def test_runs_checks_fails_constraint(self):
+ """Tests running checks on a project that violates constraints."""
+ program_config = ConfigBundle(
+ programs=ProgramList(value=[Program(name='TestProgram2')]))
+ project_config = ConfigBundle(
+ designs=DesignList(value=[Design(name='TestDesign1')]))
+
+ with self.assertRaisesRegex(AssertionError,
+ "Program name must be 'TestProgram1'"):
+ ValidConstraintSuite().run_checks(
+ program_config=program_config, project_config=project_config)
+
+ def test_runs_checks_invalid_suite(self):
+ """Tests creating a ConstraintSuite with no checks."""
+ with self.assertRaisesRegex(InvalidConstraintSuiteError,
+ 'No checks found on.*InvalidConstraintSuite'):
+ InvalidConstraintSuite()