Update cq_client and add validate command to commit_queue binary
R=akuegel@chromium.org, pgervais@chromium.org
BUG=472612, 503068
Review URL: https://codereview.chromium.org/1200863002
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@295818 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/commit_queue.py b/commit_queue.py
index b08d4fe..25ddb38 100755
--- a/commit_queue.py
+++ b/commit_queue.py
@@ -26,6 +26,7 @@
sys.path.insert(0, THIRD_PARTY_DIR)
from cq_client import cq_pb2
+from cq_client import validate_config
from protobuf26 import text_format
def usage(more):
@@ -143,6 +144,25 @@
CMDbuilders.func_usage_more = '<path-to-cq-config>'
+
+def CMDvalidate(parser, args):
+ """Validates a CQ config.
+
+ Takes a single argument - path to the CQ config to be validated. Returns 0 on
+ valid config, non-zero on invalid config. Errors and warnings are printed to
+ screen.
+ """
+ _, args = parser.parse_args(args)
+ if len(args) != 1:
+ parser.error('Expected a single path to CQ config. Got: %s' %
+ ' '.join(args))
+
+ with open(args[0]) as config_file:
+ cq_config = config_file.read()
+ return 0 if validate_config.IsValid(cq_config) else 1
+
+CMDvalidate.func_usage_more = '<path-to-cq-config>'
+
###############################################################################
## Boilerplate code
diff --git a/third_party/cq_client/README.md b/third_party/cq_client/README.md
new file mode 100644
index 0000000..d37caa6
--- /dev/null
+++ b/third_party/cq_client/README.md
@@ -0,0 +1,17 @@
+This directory contains CQ client library to be distributed to other repos. If
+you need to modify some files in this directory, please make sure that you are
+changing the canonical version of the source code and not one of the copies,
+which should only be updated as a whole using Glyco (when available, see
+http://crbug.com/489420).
+
+The canonical version is located at `https://chrome-internal.googlesource.com/
+infra/infra_internal/+/master/commit_queue/cq_client`.
+
+To generate `cq_pb2.py`, please use protoc version 2.6.1:
+
+ cd commit_queue/cq_client
+ protoc cq.proto --python_out $(pwd)
+
+Additionally, please make sure to use proto3-compatible syntax, e.g. no default
+values, no required fields. Ideally, we should use proto3 generator already,
+however alpha version thereof is still unstable.
diff --git a/third_party/cq_client/test/cq_example.cfg b/third_party/cq_client/test/cq_example.cfg
new file mode 100644
index 0000000..dc69acf
--- /dev/null
+++ b/third_party/cq_client/test/cq_example.cfg
@@ -0,0 +1,55 @@
+version: 1
+cq_name: "infra"
+cq_status_url: "https://chromium-cq-status.appspot.com"
+hide_ref_in_committed_msg: true
+commit_burst_delay: 600
+max_commit_burst: 10
+in_production: false
+git_repo_url: "http://github.com/infra/infra.git"
+target_ref: "refs/pending/heads/master"
+
+rietveld {
+ url: "https://codereview.chromium.org"
+ project_bases: "https://chromium.googlesource.com/infra/infra.git@master"
+}
+
+verifiers {
+ reviewer_lgtm: {
+ committer_list: "chromium"
+ max_wait_secs: 600
+ no_lgtm_msg: "LGTM is missing"
+ }
+
+ tree_status: {
+ tree_status_url: "https://infra-status.appspot.com"
+ }
+
+ try_job {
+ buckets {
+ name: "tryserver.blink"
+ builders { name: "android_blink_compile_dbg" }
+ builders { name: "android_blink_compile_rel" }
+ builders {
+ name: "win_blink_rel"
+ triggered: true
+ }
+ }
+ buckets {
+ name: "tryserver.chromium.linux"
+ builders {
+ name: "android_arm64_dbg_recipe"
+ }
+ builders {
+ name: "linux_chromium_rel_ng"
+ experiment_percentage: 0.1
+ }
+ }
+ buckets {
+ name: "tryserver.chromium.mac"
+ builders {
+ name: "ios_dbg_simulator_ninja"
+ experiment_percentage: 1.0
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/third_party/cq_client/test/validate_config_test.py b/third_party/cq_client/test/validate_config_test.py
new file mode 100755
index 0000000..d7c0971
--- /dev/null
+++ b/third_party/cq_client/test/validate_config_test.py
@@ -0,0 +1,52 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for tools/validate_config.py."""
+
+import mock
+import os
+import unittest
+
+from cq_client import cq_pb2
+from cq_client import validate_config
+
+
+TEST_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+class TestValidateConfig(unittest.TestCase):
+ def test_is_valid(self):
+ with open(os.path.join(TEST_DIR, 'cq_example.cfg'), 'r') as test_config:
+ self.assertTrue(validate_config.IsValid(test_config.read()))
+
+ def test_has_field(self):
+ config = cq_pb2.Config()
+
+ self.assertFalse(validate_config._HasField(config, 'version'))
+ config.version = 1
+ self.assertTrue(validate_config._HasField(config, 'version'))
+
+ self.assertFalse(validate_config._HasField(
+ config, 'rietveld.project_bases'))
+ config.rietveld.project_bases.append('foo://bar')
+ self.assertTrue(validate_config._HasField(
+ config, 'rietveld.project_bases'))
+
+ self.assertFalse(validate_config._HasField(
+ config, 'verifiers.try_job.buckets'))
+ self.assertFalse(validate_config._HasField(
+ config, 'verifiers.try_job.buckets.name'))
+
+ bucket = config.verifiers.try_job.buckets.add()
+ bucket.name = 'tryserver.chromium.linux'
+
+
+ self.assertTrue(validate_config._HasField(
+ config, 'verifiers.try_job.buckets'))
+ self.assertTrue(validate_config._HasField(
+ config, 'verifiers.try_job.buckets.name'))
+
+ config.verifiers.try_job.buckets.add()
+ self.assertFalse(validate_config._HasField(
+ config, 'verifiers.try_job.buckets.name'))
diff --git a/third_party/cq_client/validate_config.py b/third_party/cq_client/validate_config.py
new file mode 100644
index 0000000..1e34b86
--- /dev/null
+++ b/third_party/cq_client/validate_config.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""CQ config validation library."""
+
+import argparse
+# The 'from google import protobuf' below was replaced to fix an issue where
+# some users may have built-in google package installed on their system, which
+# is incompatible with cq_pb2 below. This hack can be removed after
+# http://crbug.com/503067 is resolved.
+import protobuf26 as protobuf
+import logging
+import re
+import sys
+
+from cq_client import cq_pb2
+
+
+REQUIRED_FIELDS = [
+ 'version',
+ 'rietveld',
+ 'rietveld.url',
+ 'verifiers',
+ 'cq_name',
+]
+
+LEGACY_FIELDS = [
+ 'svn_repo_url',
+ 'server_hooks_missing',
+ 'verifiers_with_patch',
+]
+
+EMAIL_REGEXP = '^[^@]+@[^@]+\.[^@]+$'
+
+
+def _HasField(message, field_path):
+ """Checks that at least one field with given path exist in the proto message.
+
+ This function correctly handles repeated fields and will make sure that each
+ repeated field will have required sub-path, e.g. if 'abc' is a repeated field
+ and field_path is 'abc.def', then the function will only return True when each
+ entry for 'abc' will contain at least one value for 'def'.
+
+ Args:
+ message (google.protobuf.message.Message): Protocol Buffer message to check.
+ field_path (string): Path to the target field separated with ".".
+
+ Return:
+ True if at least one such field is explicitly set in the message.
+ """
+ path_parts = field_path.split('.', 1)
+ field_name = path_parts[0]
+ sub_path = path_parts[1] if len(path_parts) == 2 else None
+
+ field_labels = {fd.name: fd.label for fd in message.DESCRIPTOR.fields}
+ repeated_field = (field_labels[field_name] ==
+ protobuf.descriptor.FieldDescriptor.LABEL_REPEATED)
+
+ if sub_path:
+ field = getattr(message, field_name)
+ if repeated_field:
+ if not field:
+ return False
+ return all(_HasField(entry, sub_path) for entry in field)
+ else:
+ return _HasField(field, sub_path)
+ else:
+ if repeated_field:
+ return len(getattr(message, field_name)) > 0
+ else:
+ return message.HasField(field_name)
+
+
+def IsValid(cq_config):
+ """Validates a CQ config and prints errors/warnings to the screen.
+
+ Args:
+ cq_config (string): Unparsed text format of the CQ config proto.
+
+ Returns:
+ True if the config is valid.
+ """
+ try:
+ config = cq_pb2.Config()
+ protobuf.text_format.Merge(cq_config, config)
+ except protobuf.text_format.ParseError as e:
+ logging.error('Failed to parse config as protobuf:\n%s', e)
+ return False
+
+ for fname in REQUIRED_FIELDS:
+ if not _HasField(config, fname):
+ logging.error('%s is a required field', fname)
+ return False
+
+ for fname in LEGACY_FIELDS:
+ if _HasField(config, fname):
+ logging.warn('%s is a legacy field', fname)
+
+
+ for base in config.rietveld.project_bases:
+ try:
+ re.compile(base)
+ except re.error:
+ logging.error('failed to parse "%s" in project_bases as a regexp', base)
+ return False
+
+ # TODO(sergiyb): For each field, check valid values depending on its
+ # semantics, e.g. email addresses, regular expressions etc.
+
+ return True