policy_DeviceCharging: Add BatteryChargeMode client test
Add a client and server tests for the BatteryChargeMode device policy,
which controls device charging. The ChargingPolicyTest class is in a
library because it will be used in the following CLs.
BUG=b:139201701
TEST=test_that -b sarien $DUT policy_DeviceChargingServer.BatteryChargeMode
Change-Id: Ib6bb4ebbb7bc1a8efef2845b78341f2f128e9d47
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1867573
Reviewed-by: Derek Beckett <dbeckett@chromium.org>
Commit-Queue: Daniel Campello <campello@chromium.org>
Tested-by: Daniel Campello <campello@chromium.org>
diff --git a/client/cros/enterprise/charging_policy_tests.py b/client/cros/enterprise/charging_policy_tests.py
new file mode 100644
index 0000000..8caa546
--- /dev/null
+++ b/client/cros/enterprise/charging_policy_tests.py
@@ -0,0 +1,74 @@
+# Copyright 2019 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.
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.cros.enterprise import enterprise_policy_base
+from autotest_lib.client.cros.power import power_status
+
+class ChargingPolicyTest(enterprise_policy_base.EnterprisePolicyTest):
+ """
+ A Client test that verifies that AC usage and battery charging is consistent
+ with policy settings. As of this writing, these features are only present on
+ the Wilco platform.
+ """
+ # The Wilco EC updates it's charging behavior every 10 seconds,
+ # so give ourselves 15 seconds to notice a change in behavior.
+ POLICY_CHANGE_TIMEOUT = 15
+
+ def run_once(self,
+ test_cases,
+ min_battery_level,
+ prep_policies):
+ """
+ Test a collection of cases.
+
+ @param test_cases: Collection of (policies, expected_behavior) pairs,
+ where expected_behavior is one of values accepted by
+ power_status.poll_for_charging_behavior().
+ @param min_battery_level: For the policy to affect the behavior
+ correctly, the battery level may need to be
+ above a certain percentage.
+ @param prep_policies: To test that policies P1 cause behavior B1, we
+ need to start in a state P2 where behavior B2 is
+ not B1, so we can notice the change to B1.
+ prep_policies is a dict that maps B1 => (P2, B2),
+ so that we can look up how to prep for testing
+ P1.
+ """
+ self.setup_case(enroll=True)
+
+ failures = []
+ for policies, expected_behavior in test_cases:
+ setup_policies, prep_behavior = prep_policies[expected_behavior]
+ err = self._test_policies(setup_policies, prep_behavior,
+ min_battery_level)
+ if err is not None:
+ failures.append(err)
+
+ # Now that we are set up, test the actual test case.
+ err = self._test_policies(policies, expected_behavior,
+ min_battery_level)
+ if err is not None:
+ failures.append(err)
+ if failures:
+ raise error.TestFail('Failed the following cases: {}'.format(
+ str(failures)))
+
+ def _test_policies(self, policies, expected_behavior, min_battery_level):
+ self.update_policies(device_policies=policies)
+ try:
+ self._assert_battery_is_testable(min_battery_level)
+ power_status.poll_for_charging_behavior(expected_behavior,
+ self.POLICY_CHANGE_TIMEOUT)
+ except BaseException as e:
+ msg = ('Expected to be {} using policies {}. Got this instead: {}'.
+ format(expected_behavior, policies, str(e)))
+ return msg
+ return None
+
+ def _assert_battery_is_testable(self, min_battery_level):
+ status = power_status.get_status()
+ if status.battery_full():
+ raise error.TestError('The battery is full, but should not be')
+ status.assert_battery_in_range(min_battery_level, 100)
diff --git a/client/cros/power/power_status.py b/client/cros/power/power_status.py
index 9dbfddc..4a64d7b 100644
--- a/client/cros/power/power_status.py
+++ b/client/cros/power/power_status.py
@@ -20,6 +20,7 @@
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error, enum
+from autotest_lib.client.common_lib.utils import poll_for_condition_ex
from autotest_lib.client.cros import kernel_trace
from autotest_lib.client.cros.power import power_utils
@@ -510,6 +511,16 @@
return self.battery.status.rstrip() == 'Discharging'
+ def battery_full(self):
+ """
+ Returns true if battery is currently full or false otherwise.
+ """
+ if not self.battery_path:
+ logging.warn('Unable to determine battery fullness status')
+ return False
+
+ return self.battery.status.rstrip() == 'Full'
+
def battery_discharge_ok_on_ac(self):
"""Returns True if battery is ok to discharge on AC presently.
@@ -549,6 +560,13 @@
raise error.TestError('Initial charge (%f) less than min (%f)'
% (percent_initial_charge, percent_initial_charge_min))
+ def assert_battery_in_range(self, min_level, max_level):
+ """Raise a error.TestFail if the battery level is not in range."""
+ current_percent = self.percent_current_charge()
+ if not (min_level <= current_percent <= max_level):
+ raise error.TestFail('battery must be in range [{}, {}]'.format(
+ min_level, max_level))
+
def get_status():
"""
@@ -562,6 +580,51 @@
return status
+def poll_for_charging_behavior(behavior, timeout):
+ """
+ Wait up to |timeout| seconds for the charging behavior to become |behavior|.
+
+ @param behavior: One of 'ON_AC_AND_CHARGING',
+ 'ON_AC_AND_NOT_CHARGING',
+ 'NOT_ON_AC_AND_NOT_CHARGING'.
+ @param timeout: in seconds.
+
+ @raises: error.TestFail if the behavior does not match in time, or another
+ exception if something else fails along the way.
+ """
+ ps = get_status()
+
+ def _verify_on_AC_and_charging():
+ ps.refresh()
+ if not ps.on_ac():
+ raise error.TestFail('Device is not on AC, but should be')
+ if not ps.battery_charging():
+ raise error.TestFail('Device is not charging, but should be')
+ return True
+
+ def _verify_on_AC_and_not_charging():
+ ps.refresh()
+ if not ps.on_ac():
+ raise error.TestFail('Device is not on AC, but should be')
+ if ps.battery_charging():
+ raise error.TestFail('Device is charging, but should not be')
+ return True
+
+ def _verify_not_on_AC_and_not_charging():
+ ps.refresh()
+ if ps.on_ac():
+ raise error.TestFail('Device is on AC, but should not be')
+ return True
+
+ poll_functions = {
+ 'ON_AC_AND_CHARGING' : _verify_on_AC_and_charging,
+ 'ON_AC_AND_NOT_CHARGING' : _verify_on_AC_and_not_charging,
+ 'NOT_ON_AC_AND_NOT_CHARGING': _verify_not_on_AC_and_not_charging,
+ }
+ poll_for_condition_ex(poll_functions[behavior],
+ timeout=timeout,
+ sleep_interval=1)
+
class AbstractStats(object):
"""
Common superclass for measurements of percentages per state over time.
diff --git a/client/site_tests/policy_DeviceCharging/control b/client/site_tests/policy_DeviceCharging/control
new file mode 100644
index 0000000..b662982
--- /dev/null
+++ b/client/site_tests/policy_DeviceCharging/control
@@ -0,0 +1,24 @@
+# Copyright 2019 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.
+
+AUTHOR = 'ncrews'
+NAME = 'policy_DeviceCharging'
+TIME = 'LONG'
+TEST_CATEGORY = 'General'
+TEST_CLASS = 'enterprise'
+TEST_TYPE = 'client'
+
+DOC = '''
+Verifies that the DeviceBatteryChargeMode policy works.
+
+This test is kicked off via policy_DeviceChargingServer server test. It requires
+a Servo v4 and Servo Micro attached to the DUT. Also, it requires that the
+battery is not full, and that the battery is above |MIN_BATTERY_LEVEL|, so that
+the policies can get fully tested. The server test should take care of this
+setup.
+'''
+
+args_dict = utils.args_to_dict(args)
+
+job.run_test('policy_DeviceCharging', **args_dict)
diff --git a/client/site_tests/policy_DeviceCharging/policy_DeviceCharging.py b/client/site_tests/policy_DeviceCharging/policy_DeviceCharging.py
new file mode 100644
index 0000000..205d8c6
--- /dev/null
+++ b/client/site_tests/policy_DeviceCharging/policy_DeviceCharging.py
@@ -0,0 +1,14 @@
+# Copyright 2019 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.
+
+from autotest_lib.client.cros.enterprise import charging_policy_tests
+
+
+class policy_DeviceCharging(charging_policy_tests.ChargingPolicyTest):
+ """
+ Client test for device policies that change charging behavior.
+
+ Everything is taken care of in the superclass.
+ """
+ version = 1
diff --git a/server/cros/enterprise/__init__.py b/server/cros/enterprise/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/server/cros/enterprise/__init__.py
diff --git a/server/cros/enterprise/device_policy_test.py b/server/cros/enterprise/device_policy_test.py
new file mode 100644
index 0000000..390d558
--- /dev/null
+++ b/server/cros/enterprise/device_policy_test.py
@@ -0,0 +1,27 @@
+# Copyright 2019 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.
+
+from autotest_lib.client.common_lib.cros import tpm_utils
+from autotest_lib.server import test
+
+class DevicePolicyServerTest(test.test):
+ """
+ A server test that verifies a device policy, by refreshing the TPM so the
+ DUT is not owned, and then kicking off the client test.
+
+ This class takes charge of the TPM initialization and cleanup, but child
+ tests should implement their own run_once().
+ """
+ version = 1
+
+ def warmup(self, host, *args, **kwargs):
+ """Clear TPM to ensure that client test can enroll device."""
+ super(DevicePolicyServerTest, self).warmup(host, *args, **kwargs)
+ self.host = host
+ tpm_utils.ClearTPMIfOwned(self.host)
+
+ def cleanup(self):
+ """Get the DUT back to a clean state."""
+ tpm_utils.ClearTPMIfOwned(self.host)
+ super(DevicePolicyServerTest, self).cleanup()
diff --git a/server/cros/power/utils.py b/server/cros/power/utils.py
new file mode 100644
index 0000000..253c8a8
--- /dev/null
+++ b/server/cros/power/utils.py
@@ -0,0 +1,40 @@
+# Copyright 2019 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.
+
+"""Power utils for server tests."""
+
+from autotest_lib.server import autotest
+from autotest_lib.server.cros.power import servo_charger
+
+def put_host_battery_in_range(host, min_level, max_level, timeout):
+ """
+ Charges or drains the host's battery to the specified range within the
+ timeout. This uses a servo v4 and either the power_BatteryCharge or the
+ power_BatteryDrain client test.
+
+ @param host: DUT to use
+ @param min_level: battery percentage
+ @param max_level: battery percentage
+ @param timeout: in seconds
+
+ @throws: A TestFail error if getting the current battery level, setting the
+ servo's charge state, or running either of the client tests fails.
+ """
+ current_level = host.get_battery_percentage()
+ if current_level >= min_level and current_level <= max_level:
+ return
+
+ autotest_client = autotest.Autotest(host)
+ charge_manager = servo_charger.ServoV4ChargeManager(host, host.servo)
+ if current_level < min_level:
+ charge_manager.start_charging()
+ autotest_client.run_test('power_BatteryCharge',
+ max_run_time=timeout,
+ percent_target_charge=min_level,
+ use_design_charge_capacity=False)
+ if current_level > max_level:
+ charge_manager.stop_charging()
+ autotest_client.run_test('power_BatteryDrain',
+ drain_to_percent=max_level,
+ drain_timeout=timeout)
diff --git a/server/site_tests/policy_DeviceChargingServer/control.BatteryChargeMode b/server/site_tests/policy_DeviceChargingServer/control.BatteryChargeMode
new file mode 100644
index 0000000..74ada7b
--- /dev/null
+++ b/server/site_tests/policy_DeviceChargingServer/control.BatteryChargeMode
@@ -0,0 +1,79 @@
+# Copyright 2019 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.
+
+from autotest_lib.client.common_lib import utils
+from autotest_lib.server.hosts import cros_host
+
+AUTHOR = 'ncrews'
+DEPENDENCIES = "servo"
+NAME = 'policy_DeviceChargingServer.BatteryChargeMode'
+TIME = 'LONG'
+TEST_CATEGORY = 'General'
+TEST_CLASS = 'enterprise'
+TEST_TYPE = 'server'
+ATTRIBUTES = 'suite:ent-nightly, suite:policy'
+
+DOC = """
+Ensures the DUT's battery level is in a testable range, clears the TPM if
+needed, and then runs the specified client test to verify charging behavior
+is consistent with policies.
+"""
+
+args_dict = utils.args_to_dict(args)
+servo_args = cros_host.CrosHost.get_servo_arguments(args_dict)
+
+client_test = 'policy_DeviceCharging'
+
+# When DeviceBatteryChargeMode is set to BATTERY_CHARGE_PRIMARILY_AC_USE, then
+# the DUT will not charge when above 86%. In order to test this, we need to be
+# above this threshold.
+MIN_BATTERY_LEVEL = 87
+
+# A test case consists of the policies, plus the expected power behavior.
+TEST_CASES = [
+ ({'DeviceBatteryChargeMode': 1}, # BATTERY_CHARGE_STANDARD
+ 'ON_AC_AND_CHARGING'),
+ ({'DeviceBatteryChargeMode': 2}, # BATTERY_CHARGE_EXPRESS_CHARGE
+ 'ON_AC_AND_CHARGING'),
+ ({'DeviceBatteryChargeMode': 3}, # BATTERY_CHARGE_PRIMARILY_AC_USE
+ 'ON_AC_AND_CHARGING'),
+ ({'DeviceBatteryChargeMode': 4}, # `BATTERY_CHARGE_ADAPTIVE
+ 'ON_AC_AND_CHARGING'),
+ ({'DeviceBatteryChargeMode': 5, # BATTERY_CHARGE_CUSTOM
+ 'DeviceBatteryChargeCustomStartCharging': 50,
+ 'DeviceBatteryChargeCustomStopCharging': 60},
+ 'ON_AC_AND_NOT_CHARGING'),
+ ({'DeviceBatteryChargeMode': 5, # BATTERY_CHARGE_CUSTOM
+ 'DeviceBatteryChargeCustomStartCharging': 50,
+ 'DeviceBatteryChargeCustomStopCharging': 100},
+ 'ON_AC_AND_CHARGING'),
+]
+
+# These are used to cleanup the DUT and to prep the DUT before each test case.
+# See the test for more info.
+ON_AC_AND_CHARGING_POLICIES = {
+ 'DeviceBatteryChargeMode': 1, # BATTERY_CHARGE_STANDARD
+}
+ON_AC_AND_NOT_CHARGING_POLICIES = {
+ 'DeviceBatteryChargeMode': 5, # BATTERY_CHARGE_CUSTOM
+ 'DeviceBatteryChargeCustomStartCharging': 50,
+ 'DeviceBatteryChargeCustomStopCharging': 60,
+}
+PREP_POLICIES = {
+ 'ON_AC_AND_CHARGING' : (ON_AC_AND_NOT_CHARGING_POLICIES,
+ 'ON_AC_AND_NOT_CHARGING'),
+ 'ON_AC_AND_NOT_CHARGING' : (ON_AC_AND_CHARGING_POLICIES,
+ 'ON_AC_AND_CHARGING'),
+}
+
+def run(machine):
+ host = hosts.create_host(machine, servo_args=servo_args)
+ job.run_test('policy_DeviceChargingServer',
+ host=host,
+ client_test=client_test,
+ test_cases=TEST_CASES,
+ min_battery_level=MIN_BATTERY_LEVEL,
+ prep_policies=PREP_POLICIES)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/policy_DeviceChargingServer/policy_DeviceChargingServer.py b/server/site_tests/policy_DeviceChargingServer/policy_DeviceChargingServer.py
new file mode 100644
index 0000000..a9666db
--- /dev/null
+++ b/server/site_tests/policy_DeviceChargingServer/policy_DeviceChargingServer.py
@@ -0,0 +1,46 @@
+# Copyright 2019 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.
+
+from autotest_lib.server import autotest
+from autotest_lib.server.cros.enterprise import device_policy_test
+from autotest_lib.server.cros.power import servo_charger, utils
+
+
+class policy_DeviceChargingServer(device_policy_test.DevicePolicyServerTest):
+ """
+ A variant of DevicePolicyServerTest that verifies charging policy behavior.
+ As of this writing, these features are only present on the Wilco platform.
+
+ It requires a Servo v4 USB-C and Servo Micro attached to the DUT.
+ """
+ version = 1
+
+ # To be in a testable state, the DUT has to have low enough battery that
+ # it can charge. Let's give ourselves a buffer for when the battery
+ # inevitably charges a bit in the middle of the test.
+ MAX_BATTERY_LEVEL = 95
+
+ # Allow 15 minutes for battery to charge or drain to the needed range.
+ BATTERY_CHANGE_TIMEOUT = 15 * 60
+
+ def run_once(self, host, client_test, test_cases, min_battery_level,
+ prep_policies):
+ """
+ Ensures the DUT's battery level is low enough to charge and above the
+ specified level, and then runs the specified client test. Assumes any
+ TPM stuff is dealt with in the parent class.
+ """
+ utils.put_host_battery_in_range(host, min_battery_level,
+ self.MAX_BATTERY_LEVEL,
+ self.BATTERY_CHANGE_TIMEOUT)
+ charger = servo_charger.ServoV4ChargeManager(host, host.servo)
+ charger.start_charging()
+
+ autotest_client = autotest.Autotest(host)
+ autotest_client.run_test(
+ client_test,
+ check_client_result=True,
+ test_cases=test_cases,
+ min_battery_level=min_battery_level,
+ prep_policies=prep_policies)