Commit 559d6de3 authored by tby's avatar tby Committed by Commit Bot

[Structured metrics] XML config changes.

This makes three updates to the XML config:

1. All events must now have an associated project.

2. All projects now have an ID field.

3. Events no longer need owners fields, only the project itself.

Bug: 1148168
Change-Id: I128d365819f9d54a92b3293b21b9b78a16af059c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2550150
Commit-Queue: Tony Yeoman <tby@chromium.org>
Reviewed-by: default avatarRachel Wong <wrong@chromium.org>
Cr-Commit-Position: refs/heads/master@{#830079}
parent b068d2b8
......@@ -34,19 +34,17 @@ namespace {
// 32 byte long test key, matching the size of a real key.
constexpr char kKey[] = "abcdefghijklmnopqrstuvwxyzabcdef";
// These event and metric names are used for testing.
// - event: TestEventOne
// - metric: TestValueOne
// - metric: TestValueTwo
// - event: TestEventTwo
// - metric: TestValueOne
// The name hash of "TestEventOne".
constexpr uint64_t kEventOneHash = UINT64_C(15619026293081468407);
// The name hash of "TestEventTwo".
constexpr uint64_t kEventTwoHash = UINT64_C(15791833939776536363);
// The name hash of "TestProject".
constexpr uint64_t kProjectHash = UINT64_C(17426425568333718899);
// These project, event, and metric names are used for testing.
// - project: TestProjectOne
// - event: TestEventOne
// - metric: TestMetricOne
// - metric: TestMetricTwo
// - project: TestProjectTwo
// The name hash of "TestProjectOne".
constexpr uint64_t kProjectOneHash = UINT64_C(16881314472396226433);
// The name hash of "TestProjectTwo".
constexpr uint64_t kProjectTwoHash = UINT64_C(5876808001962504629);
// The name hash of "TestMetricOne".
constexpr uint64_t kMetricOneHash = UINT64_C(637929385654885975);
......@@ -57,9 +55,7 @@ constexpr uint64_t kMetricTwoHash = UINT64_C(14083999144141567134);
constexpr char kUserId[] = "2070DF23E0D95759";
// Test values and their hashes. Hashes are the first 8 bytes of:
//
// HMAC_SHA256(concat(hex(kMetricNHash), kValueN),
// "abcdefghijklmnopqrstuvwxyzabcdef")
// HMAC_SHA256(concat(hex(kMetricNHash), kValueN), kKey)
constexpr char kValueOne[] = "value one";
constexpr char kValueTwo[] = "value two";
constexpr char kValueOneHash[] = "805B8790DC69B773";
......@@ -179,16 +175,16 @@ class KeyDataTest : public testing::Test {
};
// If there is no key store file present, check that new keys are generated for
// each event, and those keys are of the right length and different from each
// each project, and those keys are of the right length and different from each
// other.
TEST_F(KeyDataTest, GeneratesKeysForEvents) {
TEST_F(KeyDataTest, GeneratesKeysForProjects) {
StandardSetup();
histogram_tester_.ExpectUniqueSample(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kCreated,
NumberOfEvents());
const std::string key_one = GetString(KeyPath(kEventOneHash));
const std::string key_two = GetString(KeyPath(kProjectHash));
const std::string key_one = GetString(KeyPath(kProjectOneHash));
const std::string key_two = GetString(KeyPath(kProjectTwoHash));
EXPECT_EQ(key_one.size(), 32ul);
EXPECT_EQ(key_two.size(), 32ul);
......@@ -203,7 +199,7 @@ TEST_F(KeyDataTest, GeneratesDistinctKeys) {
for (int i = 0; i < 10; ++i) {
ResetState();
StandardSetup();
keys.insert(GetString(KeyPath(kEventOneHash)));
keys.insert(GetString(KeyPath(kProjectOneHash)));
histogram_tester_.ExpectUniqueSample(
"UMA.StructuredMetrics.KeyValidationState",
KeyValidationState::kCreated, NumberOfEvents() * (i + 1));
......@@ -218,7 +214,7 @@ TEST_F(KeyDataTest, ReuseExistingKeys) {
histogram_tester_.ExpectBucketCount(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kCreated,
NumberOfEvents());
const std::string key_one = GetString(KeyPath(kEventOneHash));
const std::string key_one = GetString(KeyPath(kProjectOneHash));
CommitKeyStore();
key_data_.reset();
......@@ -227,7 +223,7 @@ TEST_F(KeyDataTest, ReuseExistingKeys) {
histogram_tester_.ExpectBucketCount(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kValid,
NumberOfEvents());
const std::string key_two = GetString(KeyPath(kEventOneHash));
const std::string key_two = GetString(KeyPath(kProjectOneHash));
EXPECT_EQ(key_one, key_two);
}
......@@ -236,57 +232,56 @@ TEST_F(KeyDataTest, ReuseExistingKeys) {
// value.
TEST_F(KeyDataTest, DifferentEventsDifferentHashes) {
StandardSetup();
// Even though
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kEventTwoHash, kMetricOneHash, "value"));
key_data_->HashForEventMetric(kProjectOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kProjectTwoHash, kMetricOneHash, "value"));
ExpectNoErrors();
}
// Check that an event has different hashes for different values of the same
// metric.
// Check that an event has different hashes for different metrics with the same
// value.
TEST_F(KeyDataTest, DifferentMetricsDifferentHashes) {
StandardSetup();
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "first"),
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "second"));
key_data_->HashForEventMetric(kProjectOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kProjectOneHash, kMetricTwoHash, "value"));
ExpectNoErrors();
}
// Check that an event has different hashes for different metrics with the same
// value.
// Check that an event has different hashes for different values of the same
// metric.
TEST_F(KeyDataTest, DifferentValuesDifferentHashes) {
StandardSetup();
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kEventOneHash, kMetricTwoHash, "value"));
key_data_->HashForEventMetric(kProjectOneHash, kMetricOneHash, "first"),
key_data_->HashForEventMetric(kProjectOneHash, kMetricOneHash, "second"));
ExpectNoErrors();
}
// Ensure that KeyData::UserId is the expected value of SHA256(key).
TEST_F(KeyDataTest, CheckUserIDs) {
MakeKeyStore();
SetKeyData(kEventOneHash, kKey, 0, 90);
SetKeyData(kProjectOneHash, kKey, 0, 90);
CommitKeyStore();
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->UserEventId(kEventOneHash)), kUserId);
EXPECT_NE(HashToHex(key_data_->UserEventId(kEventTwoHash)), kUserId);
EXPECT_EQ(HashToHex(key_data_->UserEventId(kProjectOneHash)), kUserId);
EXPECT_NE(HashToHex(key_data_->UserEventId(kProjectTwoHash)), kUserId);
ExpectNoErrors();
}
// Ensure that KeyData::Hash returns expected values for a known key and value.
TEST_F(KeyDataTest, CheckHashes) {
MakeKeyStore();
SetString(KeyPath(kEventOneHash), kKey);
SetKeyData(kEventOneHash, kKey, 0, 90);
SetString(KeyPath(kProjectOneHash), kKey);
SetKeyData(kProjectOneHash, kKey, 0, 90);
CommitKeyStore();
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kEventOneHash,
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kProjectOneHash,
kMetricOneHash, kValueOne)),
kValueOneHash);
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kEventOneHash,
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kProjectOneHash,
kMetricTwoHash, kValueTwo)),
kValueTwoHash);
ExpectNoErrors();
......@@ -301,22 +296,22 @@ TEST_F(KeyDataTest, KeysRotated) {
// logic.
StandardSetup();
const uint64_t first_id = key_data_->UserEventId(kEventOneHash);
const uint64_t first_id = key_data_->UserEventId(kProjectOneHash);
const int start_day = (base::Time::Now() - base::Time::UnixEpoch()).InDays();
// TestEventOne has a default rotation period of 90 days.
EXPECT_EQ(GetInt(RotationPeriodPath(kEventOneHash)), 90);
EXPECT_EQ(GetInt(RotationPeriodPath(kProjectOneHash)), 90);
// Set the last rotation to today for testing.
SetInt(LastRotationPath(kEventOneHash), start_day);
SetInt(LastRotationPath(kProjectOneHash), start_day);
{
// Advancing by 50 days, the key should not be rotated.
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(50));
StandardSetup();
EXPECT_EQ(key_data_->UserEventId(kEventOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day);
EXPECT_EQ(key_data_->UserEventId(kProjectOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kProjectOneHash)), start_day);
}
{
......@@ -325,8 +320,8 @@ TEST_F(KeyDataTest, KeysRotated) {
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(50));
StandardSetup();
EXPECT_NE(key_data_->UserEventId(kEventOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day + 90);
EXPECT_NE(key_data_->UserEventId(kProjectOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kProjectOneHash)), start_day + 90);
}
{
......@@ -335,7 +330,7 @@ TEST_F(KeyDataTest, KeysRotated) {
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(453));
StandardSetup();
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day + 6 * 90);
EXPECT_EQ(GetInt(LastRotationPath(kProjectOneHash)), start_day + 6 * 90);
}
}
......
......@@ -27,28 +27,31 @@ namespace structured {
namespace {
// These event and metric names are used for testing.
// - event: TestEventOne
// - metric: TestMetricOne
// - metric: TestMetricTwo
// - event: TestsEventTwo
// - metric: TestMetricThree
// These project, event, and metric names are used for testing.
// - project: TestProjectOne
// - event: TestEventOne
// - metric: TestMetricOne
// - metric: TestMetricTwo
// - project: TestProjectTwo
// - event: TestEventTwo
// - metric: TestMetricThree
// - event: TestEventThree
// - metric: TestMetricFour
// To test that the right values are calculated for hashed metrics, we need to
// set up some fake keys that we know the output hashes for. kKeyData contains
// the JSON for a simple structured_metrics.json file with keys for the test
// events. The two keys are ID'd by the name hashes of "TestEventOne" and
// "TestProject", because TestEventTwo is associated with TestProject.
// projects, "TestProjectOne" and "TestProjectTwo".
// TODO(crbug.com/1016655): Once custom rotation periods have been implemented,
// change the large constants to 0.
constexpr char kKeyData[] = R"({
"keys":{
"15619026293081468407":{
"16881314472396226433":{
"rotation_period":1000000,
"last_rotation":1000000,
"key":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"17426425568333718899":{
"5876808001962504629":{
"rotation_period":1000000,
"last_rotation":1000000,
"key":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
......@@ -69,11 +72,13 @@ constexpr uint64_t kMetricOneHash = UINT64_C(637929385654885975);
constexpr uint64_t kMetricTwoHash = UINT64_C(14083999144141567134);
// The name hash of "TestMetricThree".
constexpr uint64_t kMetricThreeHash = UINT64_C(13469300759843809564);
// The name hash of "TestMetricFour".
// constexpr uint64_t kMetricFourHash = UINT64_C(13469300759843809564);
// The hex-encoded first 8 bytes of SHA256("aaa...a")
constexpr char kKeyOneId[] = "3BA3F5F43B926026";
constexpr char kProjectOneId[] = "3BA3F5F43B926026";
// The hex-encoded first 8 bytes of SHA256("bbb...b")
constexpr char kKeyTwoId[] = "BDB339768BC5E4FE";
constexpr char kProjectTwoId[] = "BDB339768BC5E4FE";
// Test values.
constexpr char kValueOne[] = "value one";
......@@ -252,7 +257,7 @@ TEST_F(StructuredMetricsProviderTest, EventsReportedCorrectly) {
{ // First event
const auto& event = uma.structured_event(0);
EXPECT_EQ(event.event_name_hash(), kEventOneHash);
EXPECT_EQ(HashToHex(event.profile_event_id()), kKeyOneId);
EXPECT_EQ(HashToHex(event.profile_event_id()), kProjectOneId);
ASSERT_EQ(event.metrics_size(), 2);
{ // First metric
......@@ -274,7 +279,7 @@ TEST_F(StructuredMetricsProviderTest, EventsReportedCorrectly) {
{ // Second event
const auto& event = uma.structured_event(1);
EXPECT_EQ(event.event_name_hash(), kEventTwoHash);
EXPECT_EQ(HashToHex(event.profile_event_id()), kKeyTwoId);
EXPECT_EQ(HashToHex(event.profile_event_id()), kProjectTwoId);
ASSERT_EQ(event.metrics_size(), 1);
{ // First metric
......@@ -313,9 +318,9 @@ TEST_F(StructuredMetricsProviderTest, EventsWithinProjectReportedWithSameID) {
// Events two and three share a project, so should have the same ID. Event
// one should have its own ID.
EXPECT_EQ(HashToHex(event_one.profile_event_id()), kKeyOneId);
EXPECT_EQ(HashToHex(event_two.profile_event_id()), kKeyTwoId);
EXPECT_EQ(HashToHex(event_three.profile_event_id()), kKeyTwoId);
EXPECT_EQ(HashToHex(event_one.profile_event_id()), kProjectOneId);
EXPECT_EQ(HashToHex(event_two.profile_event_id()), kProjectTwoId);
EXPECT_EQ(HashToHex(event_three.profile_event_id()), kProjectTwoId);
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError", 0);
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.PrefReadError", 0);
......
......@@ -36,15 +36,21 @@ class EventInfo(object):
self.name = sanitize_name(event_obj['name'])
self.name_hash = HashName(event_obj['name'])
# If a project is associated with this event, project_obj will be non-None
# and we should use the project's name as the key name hash. Otherwise, use
# the event's name as the key name hash.
if project_obj:
project_name = sanitize_name(project_obj['name'])
else:
project_name = sanitize_name(event_obj['name'])
project_name = sanitize_name(project_obj['name'])
self.project_name_hash = HashName(project_name)
id_type = project_obj['id']['text']
if id_type == 'uma':
self.project_id_type = 'kUmaId'
elif id_type == 'per-project':
self.project_id_type = 'kProjectId'
elif id_type == 'none':
self.project_id_type = 'kUnidentified'
else:
raise Exception(
"Structured metrics event '{}' has invalid id field '{}'".format(
self.name, id_type))
class MetricInfo(object):
def __init__(self, json_obj):
......
......@@ -9,6 +9,24 @@ from model import _EVENT_TYPE, _EVENTS_TYPE
from model import _PROJECT_TYPE, _PROJECTS_TYPE
def projectsHaveRequiredFields(data):
"""Check that projects have all fields required for compilation."""
for project in data[_PROJECTS_TYPE.tag][_PROJECT_TYPE.tag]:
if 'name' not in project:
raise Exception('Structured metrics project has no name')
if 'id' not in project:
raise Exception("Structured metrics project '{}' has no id field.".format(
project['name']))
name_counts = Counter(
project['name']
for project in data[_PROJECTS_TYPE.tag][_PROJECT_TYPE.tag])
for name, count in name_counts.items():
if count != 1:
raise Exception(
"Structured metrics projects have duplicate name '{}'.".format(name))
def eventsReferenceValidProjects(data):
"""Check that any project referenced by an event exists."""
projects = {
......@@ -24,26 +42,25 @@ def eventsReferenceValidProjects(data):
event['name'], project_name))
def projectAndEventNamesDontCollide(data):
"""Check that there are no events with the same name as a project."""
projects = {
project['name']
for project in data[_PROJECTS_TYPE.tag][_PROJECT_TYPE.tag]
}
def metricNamesUniqueWithinEvent(data):
"""Check that no two metrics within an event have the same name."""
for event in data[_EVENTS_TYPE.tag][_EVENT_TYPE.tag]:
if event['name'] in projects:
raise Exception(("Structured metrics event and project have the same "
"name: '{}'.").format(event['name']))
name_counts = Counter(metric['name'] for metric in event[_METRIC_TYPE.tag])
for name, count in name_counts.items():
if count != 1:
raise Exception(("Structured metrics event '{}' has duplicated metric "
"name '{}'.").format(event['name'], name))
def eventNamesUnique(data):
"""Check that no two events have the same name."""
name_counts = Counter(
event['name'] for event in data[_EVENTS_TYPE.tag][_EVENT_TYPE.tag])
for name, count in name_counts.items():
def eventNamesUniqueWithinProject(data):
"""Check that no two events in a project have the same name."""
name_counts = Counter((event['project'], event['name'])
for event in data[_EVENTS_TYPE.tag][_EVENT_TYPE.tag])
for (project, name), count in name_counts.items():
if count != 1:
raise Exception(
"Structured metrics events have duplicate name '{}'.".format(name))
"Structured metrics project '{}' has events with duplicate "
"name '{}'.".format(project, name))
def projectNamesUnique(data):
......@@ -57,19 +74,9 @@ def projectNamesUnique(data):
"Structured metrics projects have duplicate name '{}'.".format(name))
def metricNamesUniqueWithinEvent(data):
"""Check that no two metrics within an event have the same name."""
for event in data[_EVENTS_TYPE.tag][_EVENT_TYPE.tag]:
name_counts = Counter(metric['name'] for metric in event[_METRIC_TYPE.tag])
for name, count in name_counts.items():
if count != 1:
raise Exception(("Structured metrics event '{}' has duplicated metric "
"name '{}'.").format(event['name'], name))
def validate(data):
projectsHaveRequiredFields(data)
eventsReferenceValidProjects(data)
projectAndEventNamesDontCollide(data)
eventNamesUnique(data)
projectNamesUnique(data)
metricNamesUniqueWithinEvent(data)
eventNamesUniqueWithinProject(data)
projectNamesUnique(data)
......@@ -44,7 +44,8 @@ class {event.name} final : public ::metrics::structured::EventBase {{
~{event.name}() override;
static constexpr uint64_t kEventNameHash = UINT64_C({event.name_hash});
static constexpr uint64_t kProjectNameHash = UINT64_C({event.project_name_hash});\
static constexpr uint64_t kProjectNameHash = UINT64_C({event.project_name_hash});
static constexpr IdentifierType kIdType = IdentifierType::{event.project_id_type};\
{metric_code}
}};\
"""
......
......@@ -20,6 +20,7 @@ _KEEP_ORDER = lambda node: 1
_OBSOLETE_TYPE = models.TextNodeType('obsolete')
_OWNER_TYPE = models.TextNodeType('owner', single_line=True)
_ID_TYPE = models.TextNodeType('id', single_line=True)
_SUMMARY_TYPE = models.TextNodeType('summary')
_METRIC_TYPE = models.ObjectNodeType(
......@@ -67,22 +68,32 @@ _EVENTS_TYPE = models.ObjectNodeType(
models.ChildType(_EVENT_TYPE.tag, _EVENT_TYPE, multiple=True),
])
_PROJECT_TYPE = models.ObjectNodeType(
'project',
attributes=[
('name', unicode, r'^[A-Z][A-Za-z0-9.]*$'),
],
alphabetization=[
(_OBSOLETE_TYPE.tag, lambda _: 1),
(_OWNER_TYPE.tag, lambda _: 2),
(_SUMMARY_TYPE.tag, lambda _: 3),
],
extra_newlines=(1, 1, 1),
children=[
models.ChildType(_OBSOLETE_TYPE.tag, _OBSOLETE_TYPE, multiple=False),
models.ChildType(_OWNER_TYPE.tag, _OWNER_TYPE, multiple=True),
models.ChildType(_SUMMARY_TYPE.tag, _SUMMARY_TYPE, multiple=False),
])
_PROJECT_TYPE = models.ObjectNodeType('project',
attributes=[
('name', unicode,
r'^[A-Z][A-Za-z0-9.]*$'),
],
alphabetization=[
(_OBSOLETE_TYPE.tag, lambda _: 1),
(_OWNER_TYPE.tag, lambda _: 2),
(_ID_TYPE.tag, lambda _: 3),
(_SUMMARY_TYPE.tag, lambda _: 4),
],
extra_newlines=(1, 1, 1),
children=[
models.ChildType(_OBSOLETE_TYPE.tag,
_OBSOLETE_TYPE,
multiple=False),
models.ChildType(_OWNER_TYPE.tag,
_OWNER_TYPE,
multiple=True),
models.ChildType(_ID_TYPE.tag,
_ID_TYPE,
multiple=False),
models.ChildType(_SUMMARY_TYPE.tag,
_SUMMARY_TYPE,
multiple=False),
])
_PROJECTS_TYPE = models.ObjectNodeType(
'projects',
......
......@@ -5,7 +5,6 @@
<events>
<event name="CrOSActionEvent.FileOpened" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records whenever a file is opened in the File App on ChromeOS.
</summary>
......@@ -32,7 +31,6 @@
</event>
<event name="CrOSActionEvent.SearchResultLaunched" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records information about the launch of an item from ChromeOS launcher.
</summary>
......@@ -61,7 +59,6 @@
</event>
<event name="CrOSActionEvent.SettingChanged" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records when a setting is changed; only records quick settings for now.
</summary>
......@@ -100,7 +97,6 @@
</event>
<event name="CrOSActionEvent.TabEvent.TabNavigated" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records a tab activity of navigating to a new url.
</summary>
......@@ -132,7 +128,6 @@
</event>
<event name="CrOSActionEvent.TabEvent.TabOpened" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records a tab activity of opening a new url with ctrl+click.
</summary>
......@@ -165,7 +160,6 @@
</event>
<event name="CrOSActionEvent.TabEvent.TabReactivated" project="Hindsight">
<owner>charleszhao@chromium.org</owner>
<summary>
Records a tab activity of reactivating an existing tab.
</summary>
......@@ -186,8 +180,7 @@
</metric>
</event>
<event name="LauncherUsage">
<owner>tby@chromium.org</owner>
<event name="LauncherUsage" project="LauncherUsage">
<summary>
Records information about the launch of an item (such as an app or a file)
from the ChromeOS launcher. One event is recorded for every launch
......@@ -236,8 +229,7 @@
</metric>
</event>
<event name="TestEventOne">
<owner>tby@chromium.org</owner>
<event name="TestEventOne" project="TestProjectOne">
<summary>
Event for unit testing, do not use.
</summary>
......@@ -253,8 +245,7 @@
</metric>
</event>
<event name="TestEventThree" project="TestProject">
<owner>tby@chromium.org</owner>
<event name="TestEventThree" project="TestProjectTwo">
<summary>
Event for unit testing, do not use.
</summary>
......@@ -265,8 +256,7 @@
</metric>
</event>
<event name="TestEventTwo" project="TestProject">
<owner>tby@chromium.org</owner>
<event name="TestEventTwo" project="TestProjectTwo">
<summary>
Event for unit testing, do not use.
</summary>
......@@ -283,13 +273,32 @@
<project name="Hindsight">
<owner>charleszhao@chromium.org</owner>
<owner>tby@chromium.org</owner>
<id>per-project</id>
<summary>
Project for recording CrOSActions.
</summary>
</project>
<project name="TestProject">
<project name="LauncherUsage">
<owner>tby@chromium.org</owner>
<id>per-project</id>
<summary>
Records information about search results launched from the launcher.
</summary>
</project>
<project name="TestProjectOne">
<owner>tby@chromium.org</owner>
<id>none</id>
<summary>
Project for unit testing, do not use.
</summary>
</project>
<project name="TestProjectTwo">
<owner>tby@chromium.org</owner>
<id>none</id>
<summary>
Project for unit testing, do not use.
</summary>
......
......@@ -17,7 +17,7 @@ STRUCTURED_XML = path_util.GetInputFile(('tools/metrics/structured/'
def checkElementOwners(config, element_tag):
"""Check that every element in the config has at least one owner."""
"""Check that every element with the given tag has at least one owner."""
errors = []
for node in config.getElementsByTagName(element_tag):
......@@ -103,7 +103,6 @@ def main():
[projects] = config.getElementsByTagName('projects')
errors = []
errors.extend(checkElementOwners(events, 'event'))
errors.extend(checkElementOwners(projects, 'project'))
errors.extend(checkElementsNotDuplicated(events, 'event'))
errors.extend(checkElementsNotDuplicated(projects, 'project'))
......
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