Enabled create test job out of private test build.

We now allow user to have a test source build containing tests not
present in the moblab and its database. The user could fetch those tests
and select them to create jobs.

BUG=chromium:603774
TEST=manually tests and some unit test.

Change-Id: I4aecb922b006a0743ef4cb223f555fac7343c6c0
Reviewed-on: https://chromium-review.googlesource.com/351480
Commit-Ready: Michael Tang <ntang@chromium.org>
Tested-by: Michael Tang <ntang@chromium.org>
Reviewed-by: Michael Tang <ntang@chromium.org>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/frontend/afe/control_file.py b/frontend/afe/control_file.py
index 90b34ef..a88a8b4 100644
--- a/frontend/afe/control_file.py
+++ b/frontend/afe/control_file.py
@@ -8,6 +8,7 @@
 
 import common
 from autotest_lib.frontend.afe import model_logic
+from autotest_lib.frontend.afe import site_rpc_interface
 import frontend.settings
 
 AUTOTEST_DIR = os.path.abspath(os.path.join(
@@ -147,6 +148,13 @@
 
 
 def kernel_config_file(kernel, platform):
+    """Gets the kernel config.
+
+    @param kernel The kernel rpm .
+    @param platform The platform object.
+
+    @return The kernel config string or None.
+    """
     if (not kernel.endswith('.rpm') and platform and
         platform.kernel_config):
         return platform.kernel_config
@@ -154,6 +162,12 @@
 
 
 def read_control_file(test):
+    """Reads the test control file from local disk.
+
+    @param test The test name.
+
+    @return The test control file string.
+    """
     control_file = open(os.path.join(AUTOTEST_DIR, test.path))
     control_contents = control_file.read()
     control_file.close()
@@ -197,6 +211,12 @@
 
 
 def add_boilerplate_to_nested_steps(lines):
+    """Adds boilerplate magic.
+
+    @param lines The string of lines.
+
+    @returns The string lines.
+    """
     # Look for a line that begins with 'def step_init():' while
     # being flexible on spacing.  If it's found, this will be
     # a nested set of steps, so add magic to make it work.
@@ -208,13 +228,19 @@
 
 
 def format_step(item, lines):
+    """Format a line item.
+    @param item The item number.
+    @param lines The string of lines.
+
+    @returns The string lines.
+    """
     lines = indent_text(lines, '    ')
     lines = 'def step%d():\n%s' % (item, lines)
     return lines
 
 
 def get_tests_stanza(tests, is_server, prepend=None, append=None,
-                     client_control_file=''):
+                     client_control_file='', test_source_build=None):
     """ Constructs the control file test step code from a list of tests.
 
     @param tests A sequence of test control files to run.
@@ -225,6 +251,8 @@
         Defaults to [].
     @param client_control_file If specified, use this text as the body of a
         final client control file to run after tests.  is_server must be False.
+    @param test_source_build: Build to be used to retrieve test code. Default
+                              to None.
 
     @returns The control file test code to be run.
     """
@@ -233,7 +261,11 @@
         prepend = []
     if not append:
         append = []
-    raw_control_files = [read_control_file(test) for test in tests]
+    if test_source_build:
+        raw_control_files = site_rpc_interface.get_test_control_files_by_build(
+                tests, test_source_build)
+    else:
+        raw_control_files = [read_control_file(test) for test in tests]
     return _get_tests_stanza(raw_control_files, is_server, prepend, append,
                              client_control_file=client_control_file)
 
@@ -277,7 +309,13 @@
 
 def indent_text(text, indent):
     """Indent given lines of python code avoiding indenting multiline
-    quoted content (only for triple " and ' quoting for now)."""
+    quoted content (only for triple " and ' quoting for now).
+
+    @param text The string of lines.
+    @param indent The indent string.
+
+    @return The indented string.
+    """
     regex = re.compile('(\\\\*)("""|\'\'\')')
 
     res = []
@@ -355,7 +393,7 @@
 
 def generate_control(tests, kernels=None, platform=None, is_server=False,
                      profilers=(), client_control_file='', profile_only=None,
-                     upload_kernel_config=False):
+                     upload_kernel_config=False, test_source_build=None):
     """
     Generate a control file for a sequence of tests.
 
@@ -373,6 +411,8 @@
             file code that uploads the kernel config file to the client and
             tells the client of the new (local) path when compiling the kernel;
             the tests must be server side tests
+    @param test_source_build: Build to be used to retrieve test code. Default
+                              to None.
 
     @returns The control file text as a string.
     """
@@ -391,5 +431,6 @@
     prepend, append = _get_profiler_commands(profilers, is_server, profile_only)
 
     control_file_text += get_tests_stanza(tests, is_server, prepend, append,
-                                          client_control_file)
+                                          client_control_file,
+                                          test_source_build)
     return control_file_text
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index dff4165..116a327 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -751,7 +751,7 @@
 def generate_control_file(tests=(), kernel=None, label=None, profilers=(),
                           client_control_file='', use_container=False,
                           profile_only=None, upload_kernel_config=False,
-                          db_tests=True):
+                          db_tests=True, test_source_build=None):
     """
     Generates a client-side control file to load a kernel and run tests.
 
@@ -781,6 +781,8 @@
                      of test IDs which are used to retrieve the test objects
                      from the database. If False, tests is a tuple of test
                      dictionaries stored client-side in the AFE.
+    @param test_source_build: Build to be used to retrieve test code. Default
+                              to None.
 
     @returns a dict with the following keys:
         control_file: str, The control file text.
@@ -800,7 +802,8 @@
         tests=test_objects, kernels=kernel, platform=label,
         profilers=profiler_objects, is_server=cf_info['is_server'],
         client_control_file=client_control_file, profile_only=profile_only,
-        upload_kernel_config=upload_kernel_config)
+        upload_kernel_config=upload_kernel_config,
+        test_source_build=test_source_build)
     return cf_info
 
 
@@ -892,7 +895,7 @@
 def create_job_page_handler(name, priority, control_file, control_type,
                             image=None, hostless=False, firmware_rw_build=None,
                             firmware_ro_build=None, test_source_build=None,
-                            is_cloning = False, **kwargs):
+                            is_cloning=False, **kwargs):
     """\
     Create and enqueue a job.
 
@@ -1303,7 +1306,7 @@
         image = get_parameterized_autoupdate_image_url(job)
     else:
         keyvals = job.keyval_dict()
-        image =  keyvals.get('build')
+        image = keyvals.get('build')
         if not image:
             value = keyvals.get('builds')
             builds = None
diff --git a/frontend/afe/rpc_utils.py b/frontend/afe/rpc_utils.py
index f2965d2..eea15b9 100644
--- a/frontend/afe/rpc_utils.py
+++ b/frontend/afe/rpc_utils.py
@@ -268,6 +268,23 @@
     return type('TestObject', (object,), numerized_dict)
 
 
+def _check_is_server_test(test_type):
+    """Checks if the test type is a server test.
+
+    @param test_type The test type in enum integer or string.
+
+    @returns A boolean to identify if the test type is server test.
+    """
+    if test_type is not None:
+        if isinstance(test_type, basestring):
+            try:
+                test_type = control_data.CONTROL_TYPE.get_value(test_type)
+            except AttributeError:
+                return False
+        return (test_type == control_data.CONTROL_TYPE.SERVER)
+    return False
+
+
 def prepare_generate_control_file(tests, kernel, label, profilers,
                                   db_tests=True):
     if db_tests:
@@ -287,7 +304,7 @@
              'tests together (tests %s and %s differ' % (
             test1.name, test2.name)})
 
-    is_server = (test_type == control_data.CONTROL_TYPE.SERVER)
+    is_server = _check_is_server_test(test_type)
     if test_objects:
         synch_count = max(test.sync_count for test in test_objects)
     else:
diff --git a/frontend/afe/rpc_utils_unittest.py b/frontend/afe/rpc_utils_unittest.py
new file mode 100755
index 0000000..83d6f16
--- /dev/null
+++ b/frontend/afe/rpc_utils_unittest.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2016 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.
+
+"""Unit tests for frontend/afe/rpc_utils.py."""
+
+
+import unittest
+
+import common
+from autotest_lib.client.common_lib import control_data
+from autotest_lib.frontend import setup_django_environment
+from autotest_lib.frontend.afe import frontend_test_utils
+from autotest_lib.frontend.afe import rpc_utils
+
+
+class RpcUtilsTest(unittest.TestCase,
+                   frontend_test_utils.FrontendTestMixin):
+    """Unit tests for functions in rpc_utils.py."""
+    def setUp(self):
+        self._frontend_common_setup()
+
+
+    def tearDown(self):
+        self._frontend_common_teardown()
+
+
+    def testCheckIsServer(self):
+        """Ensure that test type check is correct."""
+        self.assertFalse(rpc_utils._check_is_server_test(None))
+        self.assertFalse(rpc_utils._check_is_server_test(
+            control_data.CONTROL_TYPE.CLIENT))
+        self.assertFalse(rpc_utils._check_is_server_test('Client'))
+        self.assertTrue(rpc_utils._check_is_server_test(
+            control_data.CONTROL_TYPE.SERVER))
+        self.assertTrue(rpc_utils._check_is_server_test('Server'))
+        self.assertFalse(rpc_utils._check_is_server_test('InvalidType'))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/frontend/afe/site_rpc_interface.py b/frontend/afe/site_rpc_interface.py
index 6077326..e8f8a4e 100644
--- a/frontend/afe/site_rpc_interface.py
+++ b/frontend/afe/site_rpc_interface.py
@@ -991,13 +991,12 @@
     stable_version_utils.delete(board=board)
 
 
-def get_tests_by_build(build, ignore_invalid_tests=False):
-    """Get the tests that are available for the specified build.
+def _initialize_control_file_getter(build):
+    """Get the remote control file getter.
 
-    @param build: unique name by which to refer to the image.
-    @param ignore_invalid_tests: flag on if unparsable tests are ignored.
+    @param build: unique name by which to refer to a remote build image.
 
-    @return: A sorted list of all tests that are in the build specified.
+    @return: A control file getter object.
     """
     # Stage the test artifacts.
     try:
@@ -1013,7 +1012,19 @@
                 'Failed to stage %s: %s' % (build, e))
 
     # Collect the control files specified in this build
-    cfile_getter = control_file_getter.DevServerGetter.create(build, ds)
+    return control_file_getter.DevServerGetter.create(build, ds)
+
+
+def get_tests_by_build(build, ignore_invalid_tests=True):
+    """Get the tests that are available for the specified build.
+
+    @param build: unique name by which to refer to the image.
+    @param ignore_invalid_tests: flag on if unparsable tests are ignored.
+
+    @return: A sorted list of all tests that are in the build specified.
+    """
+    # Collect the control files specified in this build
+    cfile_getter = _initialize_control_file_getter(build)
     if SuiteBase.ENABLE_CONTROLS_IN_BATCH:
         control_file_info_list = cfile_getter.get_suite_info()
         control_file_list = control_file_info_list.keys()
@@ -1078,3 +1089,32 @@
 
     test_objects = sorted(test_objects, key=lambda x: x.get('name'))
     return rpc_utils.prepare_for_serialization(test_objects)
+
+
+def get_test_control_files_by_build(tests, build, ignore_invalid_tests=False):
+    """Get the test control files that are available for the specified build.
+
+    @param tests A sequence of test objects to run.
+    @param build: unique name by which to refer to the image.
+    @param ignore_invalid_tests: flag on if unparsable tests are ignored.
+
+    @return: A sorted list of all tests that are in the build specified.
+    """
+    raw_control_files = []
+    # shortcut to avoid staging the image.
+    if not tests:
+        return raw_control_files
+
+    cfile_getter = _initialize_control_file_getter(build)
+    if SuiteBase.ENABLE_CONTROLS_IN_BATCH:
+        control_file_info_list = cfile_getter.get_suite_info()
+
+    for test in tests:
+        # Read and parse the control file
+        if SuiteBase.ENABLE_CONTROLS_IN_BATCH:
+            control_file = control_file_info_list[test.path]
+        else:
+            control_file = cfile_getter.get_control_file_contents(
+                    test.path)
+        raw_control_files.append(control_file)
+    return raw_control_files
diff --git a/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java b/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
index b308edb..2508860 100644
--- a/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
+++ b/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
@@ -57,8 +57,15 @@
         "Name of the test image to use. Example: \"x86-alex-release/R27-3837.0.0\". " +
         "If no image is specified, regular tests will use current image on the Host. " +
         "Please note that an image is required to run a test suite.");
-    private Button fetchImageTestsButton = new Button("Fetch Tests from Build");
-    private CheckBoxImpl ignoreInvalidTestsCheckBox = new CheckBoxImpl("Ignore Invalid Tests");
+    /**
+     * - When the fetch tests from build checkbox is unchecked (by default), the tests
+     * selection dropdown list is filled up by using the build on Moblab device.
+     * - You can only check the checkbox if a build is selected in the test source build
+     * dropdown list
+     * - Whenever the source build dropdown selection changes, automatically switch back
+     * to fetch tests from Moblab device.
+     */
+    private CheckBoxImpl fetchTestsCheckBox = new CheckBoxImpl("Fetch Tests from Build");
     private TextBox timeout = new TextBox();
     private ToolTip timeoutToolTip = new ToolTip(
         "?",
@@ -331,6 +338,7 @@
 
         testSourceBuildList.getElement().getStyle().setProperty("minWidth", "15em");
 
+        fetchTestsCheckBox.setEnabled(false);
         // Add the remaining widgets to the main panel
         panel.add(jobName, "create_job_name");
         panel.add(jobNameToolTip, "create_job_name");
@@ -338,8 +346,7 @@
         panel.add(image_urlToolTip, "create_image_url");
         panel.add(testSourceBuildList, "create_test_source_build");
         panel.add(testSourceBuildListToolTip, "create_test_source_build");
-        panel.add(fetchImageTestsButton, "fetch_image_tests");
-        panel.add(ignoreInvalidTestsCheckBox, "ignore_invalid_tests");
+        panel.add(fetchTestsCheckBox, "fetch_tests_from_build");
         panel.add(testSelector, "create_tests");
         panel.add(controlFilePanel, "create_edit_control");
         panel.add(hostSelector, "create_host_selector");
@@ -480,12 +487,8 @@
         controlFilePanel.setOpen(isOpen);
     }
 
-    public HasClickHandlers getFetchImageTestsButton() {
-        return fetchImageTestsButton;
-    }
-
-    public ICheckBox getIgnoreInvalidTestsCheckBox() {
-      return ignoreInvalidTestsCheckBox;
+    public ICheckBox getFetchTestsFromBuildCheckBox() {
+      return fetchTestsCheckBox;
     }
 
     public ITextBox getFirmwareRWBuild() {
diff --git a/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java b/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
index 0a37005..5219678 100644
--- a/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
+++ b/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
@@ -37,6 +37,8 @@
 import com.google.gwt.event.logical.shared.HasOpenHandlers;
 import com.google.gwt.event.logical.shared.OpenEvent;
 import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.json.client.JSONArray;
 import com.google.gwt.json.client.JSONBoolean;
 import com.google.gwt.json.client.JSONNull;
@@ -91,8 +93,7 @@
         public IButton getSubmitJobButton();
         public HasClickHandlers getCreateTemplateJobButton();
         public HasClickHandlers getResetButton();
-        public HasClickHandlers getFetchImageTestsButton();
-        public ICheckBox getIgnoreInvalidTestsCheckBox();
+        public ICheckBox getFetchTestsFromBuildCheckBox();
         public ITextBox getFirmwareRWBuild();
         public ITextBox getFirmwareROBuild();
         public ExtendedListBox getTestSourceBuildList();
@@ -354,6 +355,12 @@
 
         boolean testsFromBuild = testSelector.usingTestsFromBuild();
         params.put("db_tests", JSONBoolean.getInstance(!testsFromBuild));
+        if (testsFromBuild) {
+          String  testSourceBuild = display.getTestSourceBuildList().getSelectedValue();
+          if (testSourceBuild != null) {
+            params.put("test_source_build", new JSONString(testSourceBuild));
+          }
+        }
 
         JSONArray tests = new JSONArray();
         for (JSONObject test : testSelector.getSelectedTests()) {
@@ -465,6 +472,10 @@
             @Override
             public void onChange(ChangeEvent event) {
                 String image = display.getImageUrl().getText();
+                ICheckBox fetchTestsFromBuildCheckBox = display.getFetchTestsFromBuildCheckBox();
+                Boolean fetchedFromBuild = fetchTestsFromBuildCheckBox.getValue();
+                String currentTestSourceBuild = display.getTestSourceBuildList().getSelectedValue();
+                boolean sourceBuildChanged = false;
                 if (image.isEmpty()) {
                     display.getFirmwareRWBuild().setText("");
                     display.getFirmwareROBuild().setText("");
@@ -472,6 +483,7 @@
                     display.getFirmwareRWBuild().setEnabled(false);
                     display.getFirmwareROBuild().setEnabled(false);
                     display.getTestSourceBuildList().setEnabled(false);
+                    sourceBuildChanged = (currentTestSourceBuild != null);
                 }
                 else {
                     display.getFirmwareRWBuild().setEnabled(true);
@@ -483,7 +495,6 @@
                         builds.add(display.getFirmwareRWBuild().getText());
                     if (!display.getFirmwareROBuild().getText().isEmpty())
                         builds.add(display.getFirmwareROBuild().getText());
-                    String currentTestSourceBuild = display.getTestSourceBuildList().getSelectedValue();
                     int testSourceBuildIndex = builds.indexOf(currentTestSourceBuild);
                     display.getTestSourceBuildList().clear();
                     for (String build : builds) {
@@ -491,8 +502,20 @@
                     }
                     if (testSourceBuildIndex >= 0) {
                         display.getTestSourceBuildList().setSelectedIndex(testSourceBuildIndex);
+                    } else {
+                      sourceBuildChanged = true;
                     }
                 }
+
+                // Updates the fetch test checkbox UI
+                fetchTestsFromBuildCheckBox.setEnabled(!image.isEmpty());
+                if (sourceBuildChanged) {
+                  fetchTestsFromBuildCheckBox.setValue(false);
+                  // Fetch the test again from default Moblab device if necessary
+                  if (fetchedFromBuild) {
+                    fetchImageTests();
+                  }
+                }
             }
         };
 
@@ -635,17 +658,30 @@
             }
         });
 
-        display.getFetchImageTestsButton().addClickHandler(new ClickHandler() {
-            public void onClick(ClickEvent event) {
-                String imageUrl = display.getImageUrl().getText();
-                if (imageUrl == null || imageUrl.isEmpty()) {
-                    NotifyManager.getInstance().showMessage(
-                        "No build was specified for fetching tests.");
-                }
+        display.getTestSourceBuildList().addChangeHandler(new ChangeHandler() {
+          @Override
+          public void onChange(ChangeEvent event) {
+              Boolean fetchedTests = display.getFetchTestsFromBuildCheckBox().getValue();
+              display.getFetchTestsFromBuildCheckBox().setValue(false);
+              if (fetchedTests) {
                 fetchImageTests();
-            }
+              }
+          }
         });
 
+        display.getFetchTestsFromBuildCheckBox()
+            .addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+              @Override
+              public void onValueChange(ValueChangeEvent<Boolean> event) {
+                  String imageUrl = display.getImageUrl().getText();
+                  if (imageUrl == null || imageUrl.isEmpty()) {
+                      NotifyManager.getInstance().showMessage(
+                          "No build was specified for fetching tests.");
+                  }
+                  fetchImageTests();
+              }
+            });
+
         handleBuildChange();
 
         reset();
@@ -973,16 +1009,19 @@
     private void fetchImageTests() {
         testSelector.setImageTests(new JSONArray());
 
-        String imageUrl = display.getImageUrl().getText();
-        if (imageUrl == null || imageUrl.isEmpty()) {
-            testSelector.reset();
-            return;
+        String currentTestSourceBuild = display.getTestSourceBuildList().getSelectedValue();
+        Boolean fetchedFromBuild = display.getFetchTestsFromBuildCheckBox().getValue();
+        if (!fetchedFromBuild ||
+            currentTestSourceBuild == null || currentTestSourceBuild.isEmpty()) {
+          // Tests are from static Moblab build.
+          testSelector.setImageTests(new JSONArray());
+          testSelector.reset();
+          return;
         }
 
         JSONObject params = new JSONObject();
-        params.put("build", new JSONString(imageUrl));
-        params.put("ignore_invalid_tests", JSONBoolean.getInstance(
-              display.getIgnoreInvalidTestsCheckBox().getValue()));
+        params.put("build", new JSONString(currentTestSourceBuild));
+        params.put("ignore_invalid_tests", JSONBoolean.getInstance(true));
         rpcProxy.rpcCall("get_tests_by_build", params, new JsonRpcCallback() {
             @Override
             public void onSuccess(JSONValue result) {
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index 4f1f738..c84ad01 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -197,8 +197,7 @@
           <tr class="data-row data-row-alternate">
             <td class="field-name">Test source build: (optional)</td>
             <td class="has-tooltip" id="create_test_source_build"></td>
-            <td class="button" id="fetch_image_tests"></td>
-            <td class="checkbox" id="ignore_invalid_tests"></td>
+            <td class="checkbox" id="fetch_tests_from_build"></td>
           </tr>
         </table>
         <br>