Commit 4ea65f28 authored by Xianzhu Wang's avatar Xianzhu Wang

Correct multiple levels of depends_on and implied_by of runtime enabled features

Previously RuntimeEnabledFeatures::*Enabled() was incorrect when there
were multiple levels of depends_on or implied_by.

Now call *Enabled() of the depends_on/implied_by features instead of
getting their bool variables (which don't reflect the features' status
if there are depends_on/implied_by).

Add cycle detection of implied_by because that situation would otherwise
cause infinite recursion with this CL.

Add detection of non-origin-trial feature implied by origin trial
feature because that situation is invalid (see crbug.com/1061959)
and would otherwise cause compilation error with this CL.

Bug: 1061443, 1061959
Change-Id: I7b6cda1076f0c2a81edb5567a745d237c88259ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2104019Reviewed-by: default avatarJason Chase <chasej@chromium.org>
Cr-Commit-Position: refs/heads/master@{#751544}
parent cb35f1ee
...@@ -62,18 +62,6 @@ class BaseRuntimeFeatureWriter(json5_generator.Writer): ...@@ -62,18 +62,6 @@ class BaseRuntimeFeatureWriter(json5_generator.Writer):
feature['name']) in origin_trial_set feature['name']) in origin_trial_set
feature['data_member_name'] = self._data_member_name( feature['data_member_name'] = self._data_member_name(
feature['name']) feature['name'])
# Most features just check their is_foo_enabled_ bool
# but some depend on or are implied by other bools.
enabled_condition = feature['data_member_name']
assert not feature['implied_by'] or not feature[
'depends_on'], 'Only one of implied_by and depends_on is allowed'
for implied_by_name in feature['implied_by']:
enabled_condition += ' || ' + self._data_member_name(
implied_by_name)
for dependant_name in feature['depends_on']:
enabled_condition += ' && ' + self._data_member_name(
dependant_name)
feature['enabled_condition'] = enabled_condition
# If 'status' is a dict, add the values for all the not-mentioned platforms too. # If 'status' is a dict, add the values for all the not-mentioned platforms too.
if isinstance(feature['status'], dict): if isinstance(feature['status'], dict):
feature['status'] = self._status_with_all_platforms( feature['status'] = self._status_with_all_platforms(
......
...@@ -5,40 +5,55 @@ ...@@ -5,40 +5,55 @@
from collections import defaultdict from collections import defaultdict
def _runtime_features_graph_sanity_check(features): def _error_message(message, feature, other_feature=None):
message = 'runtime_enabled_features.json5: {}: {}'.format(feature, message)
if other_feature:
message += ': {}'.format(other_feature)
return message
def _validate_runtime_features_graph(features):
""" """
Raises AssertionError when sanity check failed. Raises AssertionError when sanity check failed.
@param features: a List[Dict]. Each Dict must have key 'depends_on' and 'name' @param features: a List[Dict]. See origin_trials().
@returns None @returns None
""" """
feature_pool = {str(f['name']) for f in features} feature_pool = {str(f['name']) for f in features}
origin_trial_pool = {
str(f['name'])
for f in features if f['origin_trial_feature_name']
}
for f in features: for f in features:
assert not f['implied_by'] or not f['depends_on'], _error_message(
'Only one of implied_by and depends_on is allowed', f['name'])
for d in f['depends_on']: for d in f['depends_on']:
assert d in feature_pool, "{} not found in runtime_enabled_features.json5".format( assert d in feature_pool, _error_message(
d) 'Depends on non-existent-feature', f['name'], d)
for i in f['implied_by']:
assert i in feature_pool, _error_message(
'Implied by non-existent-feature', f['name'], i)
assert f['origin_trial_feature_name'] or i not in origin_trial_pool, \
_error_message(
'A feature must be in origin trial if implied by an origin trial feature',
f['name'], i)
def cyclic(features):
"""
Returns True if the runtime features graph contains a cycle
@returns bool
"""
graph = { graph = {
str(feature['name']): feature['depends_on'] str(feature['name']): feature['depends_on'] + feature['implied_by']
for feature in features for feature in features
} }
path = set() path = set()
def visit(vertex): def has_cycle(vertex):
path.add(vertex) path.add(vertex)
for neighbor in graph[vertex]: for neighbor in graph[vertex]:
if neighbor in path or visit(neighbor): if neighbor in path or has_cycle(neighbor):
return True return True
path.remove(vertex) path.remove(vertex)
return False return False
return any(visit(str(f['name'])) for f in features) for f in features:
assert not has_cycle(str(f['name'])), _error_message(
assert not cyclic(features), "Cycle found in dependency graph" 'Cycle found in depends_on/implied_by graph', f['name'])
def origin_trials(features): def origin_trials(features):
...@@ -48,10 +63,12 @@ def origin_trials(features): ...@@ -48,10 +63,12 @@ def origin_trials(features):
or any of its dependencies are in origin trial. Propagate dependency or any of its dependencies are in origin trial. Propagate dependency
tag use DFS can find all features that are in origin trial. tag use DFS can find all features that are in origin trial.
@param features: a List[Dict]. Each Dict much has key named 'depends_on' @param features: a List[Dict]. Each Dict must have keys 'name',
'depends_on', 'implied_by' and 'origin_trial_feature_name'
(see runtime_enabled_features.json5).
@returns Set[str(runtime feature name)] @returns Set[str(runtime feature name)]
""" """
_runtime_features_graph_sanity_check(features) _validate_runtime_features_graph(features)
origin_trials_set = set() origin_trials_set = set()
......
...@@ -8,81 +8,80 @@ import make_runtime_features_utilities as util ...@@ -8,81 +8,80 @@ import make_runtime_features_utilities as util
from blinkbuild.name_style_converter import NameStyleConverter from blinkbuild.name_style_converter import NameStyleConverter
def _feature(name,
depends_on=[],
implied_by=[],
origin_trial_feature_name=None):
return {
'name': name,
'depends_on': depends_on,
'implied_by': implied_by,
'origin_trial_feature_name': origin_trial_feature_name
}
class MakeRuntimeFeaturesUtilitiesTest(unittest.TestCase): class MakeRuntimeFeaturesUtilitiesTest(unittest.TestCase):
def test_cycle_in_dependency(self): def test_cycle(self):
# Cycle: 'c' => 'd' => 'e' => 'c' # Cycle: 'c' => 'd' => 'e' => 'c'
graph = {'a': ['b'], 'b': [], 'c': ['a', 'd'], 'd': ['e'], 'e': ['c']} with self.assertRaisesRegexp(
with self.assertRaises(AssertionError): AssertionError, 'Cycle found in depends_on/implied_by graph'):
util.origin_trials([{ util.origin_trials([
'name': name, _feature('a', depends_on=['b']),
'depends_on': deps _feature('b'),
} for name, deps in graph.items()]) _feature('c', implied_by=['a', 'd']),
_feature('d', depends_on=['e']),
_feature('e', implied_by=['c'])
])
def test_bad_dependency(self): def test_bad_dependency(self):
with self.assertRaises(AssertionError): with self.assertRaisesRegexp(AssertionError,
util.origin_trials([{'name': 'a', 'depends_on': 'x'}]) 'a: Depends on non-existent-feature: x'):
util.origin_trials([_feature('a', depends_on=['x'])])
def test_bad_implication(self):
with self.assertRaisesRegexp(AssertionError,
'a: Implied by non-existent-feature: x'):
util.origin_trials([_feature('a', implied_by=['x'])])
with self.assertRaisesRegexp(
AssertionError,
'a: A feature must be in origin trial if implied by an origin trial feature: b'
):
util.origin_trials([
_feature('a', implied_by=['b']),
_feature('b', origin_trial_feature_name='b')
])
def test_in_origin_trials_flag(self): def test_both_dependency_and_implication(self):
with self.assertRaisesRegexp(
AssertionError,
'c: Only one of implied_by and depends_on is allowed'):
util.origin_trials([
_feature('a'),
_feature('b'),
_feature('c', depends_on=['a'], implied_by=['b'])
])
def test_origin_trials(self):
features = [ features = [
{ _feature(NameStyleConverter('a')),
'name': NameStyleConverter('a'), _feature(
'depends_on': [], NameStyleConverter('b'),
'origin_trial_feature_name': None depends_on=['a'],
}, origin_trial_feature_name='b'),
{ _feature(NameStyleConverter('c'), depends_on=['b']),
'name': NameStyleConverter('b'), _feature(NameStyleConverter('d'), depends_on=['b']),
'depends_on': ['a'], _feature(NameStyleConverter('e'), depends_on=['d'])
'origin_trial_feature_name': 'OriginTrials'
},
{
'name': NameStyleConverter('c'),
'depends_on': ['b'],
'origin_trial_feature_name': None
},
{
'name': NameStyleConverter('d'),
'depends_on': ['b'],
'origin_trial_feature_name': None
},
{
'name': NameStyleConverter('e'),
'depends_on': ['d'],
'origin_trial_feature_name': None
},
] ]
self.assertSetEqual(util.origin_trials(features), {'b', 'c', 'd', 'e'}) self.assertSetEqual(util.origin_trials(features), {'b', 'c', 'd', 'e'})
features = [{ features = [
'name': 'a', _feature('a'),
'depends_on': ['x'], _feature('b', depends_on=['x', 'y']),
'origin_trial_feature_name': None _feature('c', depends_on=['y', 'z']),
}, _feature('x', depends_on=['a']),
{ _feature('y', depends_on=['x'], origin_trial_feature_name='y'),
'name': 'b', _feature('z', depends_on=['y'])
'depends_on': ['x', 'y'], ]
'origin_trial_feature_name': None
},
{
'name': 'c',
'depends_on': ['y', 'z'],
'origin_trial_feature_name': None
},
{
'name': 'x',
'depends_on': [],
'origin_trial_feature_name': None
},
{
'name': 'y',
'depends_on': ['x'],
'origin_trial_feature_name': 'y'
},
{
'name': 'z',
'depends_on': ['y'],
'origin_trial_feature_name': None
}]
self.assertSetEqual(util.origin_trials(features), {'b', 'c', 'y', 'z'}) self.assertSetEqual(util.origin_trials(features), {'b', 'c', 'y', 'z'})
......
...@@ -87,15 +87,19 @@ bool RuntimeEnabledFeatures::{{feature.name}}Enabled(const FeatureContext* conte ...@@ -87,15 +87,19 @@ bool RuntimeEnabledFeatures::{{feature.name}}Enabled(const FeatureContext* conte
if (!RuntimeEnabledFeatures::{{depends_on}}Enabled(context)) if (!RuntimeEnabledFeatures::{{depends_on}}Enabled(context))
return false; return false;
{% endfor %} {% endfor %}
if (RuntimeEnabledFeatures::{{feature.name}}EnabledByRuntimeFlag()) {% for implied_by in feature.implied_by %}
if (RuntimeEnabledFeatures::{{implied_by}}Enabled(context))
return true; return true;
{% if not feature.origin_trial_feature_name %} {% endfor %}
if ({{feature.data_member_name}})
return true;
{% if not feature.origin_trial_feature_name %}
// The feature does not have an origin trial name and its runtime flag // The feature does not have an origin trial name and its runtime flag
// is not enabled. // is not enabled.
return false; return false;
{% else %} {% else %}
return context && context->FeatureEnabled(OriginTrialFeature::k{{feature.name}}); return context && context->FeatureEnabled(OriginTrialFeature::k{{feature.name}});
{% endif %} {% endif %}
} }
{% endfor %} {% endfor %}
......
...@@ -63,8 +63,18 @@ class PLATFORM_EXPORT RuntimeEnabledFeatures { ...@@ -63,8 +63,18 @@ class PLATFORM_EXPORT RuntimeEnabledFeatures {
{% for feature in features %} {% for feature in features %}
{% if not feature.in_origin_trial %} {% if not feature.in_origin_trial %}
static void Set{{feature.name}}Enabled(bool enabled) { {{feature.data_member_name}} = enabled; } static void Set{{feature.name}}Enabled(bool enabled) { {{feature.data_member_name}} = enabled; }
static bool {{feature.name}}Enabled() { return {{feature.enabled_condition}}; } static bool {{feature.name}}Enabled() {
static bool {{feature.name}}Enabled(const FeatureContext*) { return {{feature.enabled_condition}}; } {% for depends_on in feature.depends_on %}
if (!{{depends_on}}Enabled())
return false;
{% endfor %}
{% for implied_by in feature.implied_by %}
if ({{implied_by}}Enabled())
return true;
{% endfor %}
return {{feature.data_member_name}};
}
static bool {{feature.name}}Enabled(const FeatureContext*) { return {{feature.name}}Enabled(); }
{% endif %} {% endif %}
{% endfor %} {% endfor %}
...@@ -81,7 +91,7 @@ class PLATFORM_EXPORT RuntimeEnabledFeatures { ...@@ -81,7 +91,7 @@ class PLATFORM_EXPORT RuntimeEnabledFeatures {
{% for feature in origin_trial_controlled_features %} {% for feature in origin_trial_controlled_features %}
static void Set{{feature.name}}Enabled(bool enabled) { {{feature.data_member_name}} = enabled; } static void Set{{feature.name}}Enabled(bool enabled) { {{feature.data_member_name}} = enabled; }
static bool {{feature.name}}EnabledByRuntimeFlag() { return {{feature.enabled_condition}}; } static bool {{feature.name}}EnabledByRuntimeFlag() { return {{feature.name}}Enabled(nullptr); }
static bool {{feature.name}}Enabled(const FeatureContext*); static bool {{feature.name}}Enabled(const FeatureContext*);
{% endfor %} {% endfor %}
......
...@@ -1897,6 +1897,7 @@ jumbo_source_set("blink_platform_unittests_sources") { ...@@ -1897,6 +1897,7 @@ jumbo_source_set("blink_platform_unittests_sources") {
"peerconnection/two_keys_adapter_map_unittest.cc", "peerconnection/two_keys_adapter_map_unittest.cc",
"peerconnection/webrtc_audio_sink_test.cc", "peerconnection/webrtc_audio_sink_test.cc",
"peerconnection/webrtc_video_track_source_test.cc", "peerconnection/webrtc_video_track_source_test.cc",
"runtime_enabled_features_test.cc",
"text/bidi_resolver_test.cc", "text/bidi_resolver_test.cc",
"text/bidi_test_harness.h", "text/bidi_test_harness.h",
"text/capitalize_test.cc", "text/capitalize_test.cc",
......
...@@ -1638,6 +1638,20 @@ ...@@ -1638,6 +1638,20 @@
name: "SystemWakeLock", name: "SystemWakeLock",
status: "experimental", status: "experimental",
}, },
// For unit tests.
{
name: "TestFeature",
},
// For unit tests.
{
name: "TestFeatureDependent",
depends_on: ["TestFeatureImplied"],
},
// For unit tests.
{
name: "TestFeatureImplied",
implied_by: ["TestFeature"],
},
{ {
name: "TextDetector", name: "TextDetector",
status: "experimental", status: "experimental",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment