Move test scripts to scripts/test

R=aerotwist@chromium.org, liviurau@chromium.org, machenbach@chromium.org

Bug: 1018122
Change-Id: I7cc3a1a9ab01c649b3c7fe8ba5d5254adc631634
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1880033
Commit-Queue: Yang Guo <yangguo@chromium.org>
Reviewed-by: Liviu Rau <liviurau@chromium.org>
Reviewed-by: Michael Achenbach <machenbach@chromium.org>
diff --git a/scripts/build/build_debug_applications.py b/scripts/build/build_debug_applications.py
old mode 100755
new mode 100644
diff --git a/scripts/build/build_release_applications.py b/scripts/build/build_release_applications.py
old mode 100755
new mode 100644
index 7dff77e..2a1bedd
--- a/scripts/build/build_release_applications.py
+++ b/scripts/build/build_release_applications.py
@@ -23,15 +23,13 @@
 from modular_build import read_file, write_file, bail_error
 import modular_build
 import rjsmin
+import special_case_namespaces
 
 try:
     import simplejson as json
 except ImportError:
     import json
 
-special_case_namespaces_path = path.join(path.dirname(path.dirname(path.abspath(__file__))), 'special_case_namespaces.json')
-
-
 def main(argv):
     try:
         input_path_flag_index = argv.index('--input_path')
@@ -95,8 +93,7 @@
         self.descriptors = descriptors
         self.application_dir = application_dir
         self.output_dir = output_dir
-        with open(special_case_namespaces_path) as json_file:
-            self._special_case_namespaces = json.load(json_file)
+        self._special_case_namespaces = special_case_namespaces.special_case_namespaces
 
     def app_file(self, extension):
         return self.application_name + '.' + extension
diff --git a/scripts/build/code_generator_frontend.py b/scripts/build/code_generator_frontend.py
old mode 100755
new mode 100644
diff --git a/scripts/build/copy_devtools_modules.py b/scripts/build/copy_devtools_modules.py
old mode 100755
new mode 100644
diff --git a/scripts/dependency_preprocessor.py b/scripts/build/dependency_preprocessor.py
similarity index 92%
rename from scripts/dependency_preprocessor.py
rename to scripts/build/dependency_preprocessor.py
index 344a8cc..e312430 100644
--- a/scripts/dependency_preprocessor.py
+++ b/scripts/build/dependency_preprocessor.py
@@ -18,13 +18,13 @@
 import re
 import shutil
 
+import special_case_namespaces
+
 try:
     import simplejson as json
 except ImportError:
     import json
 
-special_case_namespaces_path = path.join(path.dirname(path.abspath(__file__)), 'special_case_namespaces.json')
-
 
 class DependencyPreprocessor(object):
 
@@ -34,8 +34,7 @@
         self.module_descriptors = descriptors.modules
         self.modules = set(self.descriptors.sorted_modules())
         shutil.copytree(devtools_frontend_path, self.temp_frontend_path)
-        with open(special_case_namespaces_path) as json_file:
-            self._special_case_namespaces = json.load(json_file)
+        self._special_case_namespaces = special_case_namespaces.special_case_namespaces
 
     def enforce_dependencies(self):
         arg_list = []
diff --git a/scripts/build/devtools_file_hashes.py b/scripts/build/devtools_file_hashes.py
old mode 100755
new mode 100644
diff --git a/scripts/build/generate_devtools_extension_api.py b/scripts/build/generate_devtools_extension_api.py
old mode 100755
new mode 100644
diff --git a/scripts/build/generate_devtools_grd.py b/scripts/build/generate_devtools_grd.py
old mode 100755
new mode 100644
diff --git a/scripts/build/generate_protocol_externs.py b/scripts/build/generate_protocol_externs.py
old mode 100755
new mode 100644
diff --git a/scripts/build/generate_supported_css.py b/scripts/build/generate_supported_css.py
old mode 100755
new mode 100644
diff --git a/scripts/build/modular_build.py b/scripts/build/modular_build.py
old mode 100755
new mode 100644
diff --git a/scripts/build/rjsmin.py b/scripts/build/rjsmin.py
old mode 100755
new mode 100644
diff --git a/scripts/special_case_namespaces.json b/scripts/build/special_case_namespaces.json
similarity index 100%
rename from scripts/special_case_namespaces.json
rename to scripts/build/special_case_namespaces.json
diff --git a/scripts/build/special_case_namespaces.py b/scripts/build/special_case_namespaces.py
new file mode 100644
index 0000000..d99beb5
--- /dev/null
+++ b/scripts/build/special_case_namespaces.py
@@ -0,0 +1,10 @@
+# Copyright 2019 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.
+
+import json
+from os import path
+
+special_case_namespaces_path = path.join(path.dirname(path.abspath(__file__)), 'special_case_namespaces.json')
+with open(special_case_namespaces_path) as json_file:
+    special_case_namespaces = json.load(json_file)
diff --git a/scripts/compile_frontend.py b/scripts/compile_frontend.py
index 58f1bd6..564071d 100755
--- a/scripts/compile_frontend.py
+++ b/scripts/compile_frontend.py
@@ -27,381 +27,6 @@
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-import argparse
-import os
-import os.path as path
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
+from test import run_type_check
 
-from build import modular_build
-from build import generate_protocol_externs
-
-import dependency_preprocessor
-import devtools_paths
-import utils
-
-try:
-    import simplejson as json
-except ImportError:
-    import json
-
-is_cygwin = sys.platform == 'cygwin'
-
-
-def popen(arguments):
-    return subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-
-
-def to_platform_path(filepath):
-    if not is_cygwin:
-        return filepath
-    return re.sub(r'^/cygdrive/(\w)', '\\1:', filepath)
-
-
-def to_platform_path_exact(filepath):
-    if not is_cygwin:
-        return filepath
-    output, _ = popen(['cygpath', '-w', filepath]).communicate()
-    # pylint: disable=E1103
-    return output.strip().replace('\\', '\\\\')
-
-
-SCRIPTS_PATH = path.dirname(path.abspath(__file__))
-DEVTOOLS_PATH = path.dirname(SCRIPTS_PATH)
-ROOT_PATH = devtools_paths.root_path()
-BROWSER_PROTOCOL_PATH = path.join(ROOT_PATH, 'third_party', 'blink', 'renderer', 'core', 'inspector', 'browser_protocol.pdl')
-# TODO(dgozman): move these checks to v8.
-JS_PROTOCOL_PATH = path.join(ROOT_PATH, 'v8', 'include', 'js_protocol.pdl')
-DEVTOOLS_FRONTEND_PATH = path.join(DEVTOOLS_PATH, 'front_end')
-GLOBAL_EXTERNS_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'externs.js'))
-DEFAULT_PROTOCOL_EXTERNS_FILE = path.join(DEVTOOLS_FRONTEND_PATH, 'protocol_externs.js')
-RUNTIME_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'Runtime.js'))
-ROOT_MODULE_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'root.js'))
-
-CLOSURE_COMPILER_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'closure', 'compiler.jar'))
-CLOSURE_RUNNER_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'closure', 'closure_runner', 'closure_runner.jar'))
-JSDOC_VALIDATOR_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'jsdoc_validator', 'jsdoc_validator.jar'))
-
-TYPE_CHECKED_JSDOC_TAGS_LIST = ['param', 'return', 'type', 'enum']
-TYPE_CHECKED_JSDOC_TAGS_OR = '|'.join(TYPE_CHECKED_JSDOC_TAGS_LIST)
-
-# Basic regex for invalid JsDoc types: an object type name ([A-Z][_A-Za-z0-9.]+[A-Za-z0-9]) not preceded by '!', '?', ':' (this, new), or '.' (object property).
-INVALID_TYPE_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR +
-                                r')\s*\{.*(?<![!?:._A-Za-z0-9])([A-Z][_A-Za-z0-9.]+[A-Za-z0-9])[^/]*\}')
-INVALID_TYPE_DESIGNATOR_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR + r')\s*.*(?<![{: ])([?!])=?\}')
-INVALID_NON_OBJECT_TYPE_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR + r')\s*\{.*(![a-z]+)[^/]*\}')
-ERROR_WARNING_REGEX = re.compile(r'WARNING|ERROR')
-LOADED_CSS_REGEX = re.compile(r'(?:registerRequiredCSS|WebInspector\.View\.createStyleElement)\s*\(\s*"(.+)"\s*\)')
-
-JAVA_BUILD_REGEX = re.compile(r'\w+ version "(\d+)\.(\d+)')
-
-
-def log_error(message):
-    print 'ERROR: ' + message
-
-
-def error_excepthook(exctype, value, traceback):
-    print 'ERROR:'
-    sys.__excepthook__(exctype, value, traceback)
-
-
-sys.excepthook = error_excepthook
-
-APPLICATION_DESCRIPTORS = [
-    'inspector',
-    'toolbox',
-    'integration_test_runner',
-    'formatter_worker',
-    'heap_snapshot_worker',
-]
-
-SKIPPED_NAMESPACES = {
-    'Console',  # Closure uses Console as a namespace item so we cannot override it right now.
-    'Gonzales',  # third party module defined in front_end/externs.js
-    'Terminal',  # third party module defined in front_end/externs.js
-}
-
-
-def has_errors(output):
-    return re.search(ERROR_WARNING_REGEX, output) is not None
-
-
-class JSDocChecker:
-
-    def __init__(self, descriptors, java_exec):
-        self._error_found = False
-        self._all_files = descriptors.all_compiled_files()
-        self._java_exec = java_exec
-
-    def check(self):
-        print 'Verifying JSDoc comments...'
-        self._verify_jsdoc()
-        self._run_jsdoc_validator()
-        return self._error_found
-
-    def _run_jsdoc_validator(self):
-        files = [to_platform_path(f) for f in self._all_files]
-        file_list = tempfile.NamedTemporaryFile(mode='wt', delete=False)
-        try:
-            file_list.write('\n'.join(files))
-        finally:
-            file_list.close()
-        proc = popen(self._java_exec + ['-jar', JSDOC_VALIDATOR_JAR, '--files-list-name', to_platform_path_exact(file_list.name)])
-        (out, _) = proc.communicate()
-        if out:
-            print('JSDoc validator output:%s%s' % (os.linesep, out))
-            self._error_found = True
-        os.remove(file_list.name)
-
-    def _verify_jsdoc(self):
-        for full_file_name in self._all_files:
-            line_index = 0
-            with open(full_file_name, 'r') as sourceFile:
-                for line in sourceFile:
-                    line_index += 1
-                    if line.rstrip():
-                        self._verify_jsdoc_line(full_file_name, line_index, line)
-
-    def _verify_jsdoc_line(self, file_name, line_index, line):
-
-        def print_error(message, error_position):
-            print '%s:%s: ERROR - %s%s%s%s%s%s' % (file_name, line_index, message, os.linesep, line, os.linesep,
-                                                   ' ' * error_position + '^', os.linesep)
-
-        known_css = {}
-        match = re.search(INVALID_TYPE_REGEX, line)
-        if match:
-            print_error('Type "%s" nullability not marked explicitly with "?" (nullable) or "!" (non-nullable)' % match.group(1),
-                        match.start(1))
-            self._error_found = True
-
-        match = re.search(INVALID_NON_OBJECT_TYPE_REGEX, line)
-        if match:
-            print_error('Non-object type explicitly marked with "!" (non-nullable), which is the default and should be omitted',
-                        match.start(1))
-            self._error_found = True
-
-        match = re.search(INVALID_TYPE_DESIGNATOR_REGEX, line)
-        if match:
-            print_error('Type nullability indicator misplaced, should precede type', match.start(1))
-            self._error_found = True
-
-        match = re.search(LOADED_CSS_REGEX, line)
-        if match:
-            css_file = path.join(DEVTOOLS_FRONTEND_PATH, match.group(1))
-            exists = known_css.get(css_file)
-            if exists is None:
-                exists = path.isfile(css_file)
-                known_css[css_file] = exists
-            if not exists:
-                print_error('Dynamically loaded CSS stylesheet is missing in the source tree', match.start(1))
-                self._error_found = True
-
-
-def find_java():
-    required_major = 1
-    required_minor = 7
-    exec_command = None
-    has_server_jvm = True
-    java_path = utils.which('java')
-
-    if not java_path:
-        print 'NOTE: No Java executable found in $PATH.'
-        sys.exit(1)
-
-    is_ok = False
-    java_version_out, _ = popen([java_path, '-version']).communicate()
-    # pylint: disable=E1103
-    match = re.search(JAVA_BUILD_REGEX, java_version_out)
-    if match:
-        major = int(match.group(1))
-        minor = int(match.group(2))
-        is_ok = major > required_major or major == required_major and minor >= required_minor
-    if is_ok:
-        exec_command = [java_path, '-Xms1024m', '-server', '-XX:+TieredCompilation']
-        check_server_proc = popen(exec_command + ['-version'])
-        check_server_proc.communicate()
-        if check_server_proc.returncode != 0:
-            # Not all Java installs have server JVMs.
-            exec_command = exec_command.remove('-server')
-            has_server_jvm = False
-
-    if not is_ok:
-        print 'NOTE: Java executable version %d.%d or above not found in $PATH.' % (required_major, required_minor)
-        sys.exit(1)
-    print 'Java executable: %s%s' % (java_path, '' if has_server_jvm else ' (no server JVM)')
-    return exec_command
-
-
-common_closure_args = [
-    '--summary_detail_level',
-    '3',
-    '--jscomp_error',
-    'visibility',
-    '--jscomp_warning',
-    'missingOverride',
-    '--compilation_level',
-    'SIMPLE_OPTIMIZATIONS',
-    '--warning_level',
-    'VERBOSE',
-    '--language_in=ECMASCRIPT_NEXT',
-    '--language_out=ES5_STRICT',
-    '--extra_annotation_name',
-    'suppressReceiverCheck',
-    '--extra_annotation_name',
-    'suppressGlobalPropertiesCheck',
-    '--checks-only',
-]
-
-GENERATED_SKIP_COMPILATION_FILES = [
-    'SupportedCSSProperties.js',
-    'InspectorBackendCommands.js',
-]
-
-
-def check_conditional_dependencies(modules_by_name):
-    errors_found = False
-    for name in modules_by_name:
-        if 'test_runner' in name:
-            continue
-        for dep_name in modules_by_name[name].get('dependencies', []):
-            dependency = modules_by_name[dep_name]
-            if dependency.get('experiment') or dependency.get('condition'):
-                log_error('Module "%s" may not depend on the conditional module "%s"' % (name, dep_name))
-                errors_found = True
-    return errors_found
-
-
-def prepare_closure_frontend_compile(temp_devtools_path, descriptors, namespace_externs_path, protocol_externs_file):
-    temp_frontend_path = path.join(temp_devtools_path, 'front_end')
-    checker = dependency_preprocessor.DependencyPreprocessor(descriptors, temp_frontend_path, DEVTOOLS_FRONTEND_PATH)
-    checker.enforce_dependencies()
-
-    command = common_closure_args + [
-        '--externs',
-        to_platform_path(GLOBAL_EXTERNS_FILE),
-        '--externs',
-        namespace_externs_path,
-        '--js',
-        RUNTIME_FILE,
-        '--js',
-        ROOT_MODULE_FILE,
-    ]
-
-    all_files = descriptors.all_compiled_files()
-    args = []
-    for file in all_files:
-        args.extend(['--js', file])
-        if "InspectorBackend.js" in file:
-            args.extend(['--js', protocol_externs_file])
-
-    for file in GENERATED_SKIP_COMPILATION_FILES:
-        # Write a dummy file for skipped compilation files that are autogenerated.
-        # We don't type-check this file, but we import them via ES modules
-        generated_file = path.join(temp_frontend_path, file)
-        modular_build.write_file(generated_file, '')
-        args.extend(['--js', generated_file])
-
-    command += args
-    command = [arg.replace(DEVTOOLS_FRONTEND_PATH, temp_frontend_path) for arg in command]
-    compiler_args_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
-    try:
-        compiler_args_file.write('devtools_frontend %s' % (' '.join(command)))
-    finally:
-        compiler_args_file.close()
-    return compiler_args_file.name
-
-
-def generate_namespace_externs(modules_by_name):
-    special_case_namespaces_path = path.join(path.dirname(path.abspath(__file__)), 'special_case_namespaces.json')
-    with open(special_case_namespaces_path) as json_file:
-        special_case_namespaces = json.load(json_file)
-
-    def map_module_to_namespace(module):
-        return special_case_namespaces.get(module, to_camel_case(module))
-
-    def to_camel_case(snake_string):
-        components = snake_string.split('_')
-        return ''.join(x.title() for x in components)
-
-    all_namespaces = [map_module_to_namespace(module) for module in modules_by_name]
-    namespaces = [namespace for namespace in all_namespaces if namespace not in SKIPPED_NAMESPACES]
-    namespaces.sort()
-    namespace_externs_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
-    try:
-        namespace_externs_file.write('var Root = {};\n')
-        for namespace in namespaces:
-            namespace_externs_file.write('var %s = {};\n' % namespace)
-    finally:
-        namespace_externs_file.close()
-    namespace_externs_path = to_platform_path(namespace_externs_file.name)
-    return namespace_externs_path
-
-
-def main():
-    protocol_externs_file = DEFAULT_PROTOCOL_EXTERNS_FILE
-    errors_found = False
-    parser = argparse.ArgumentParser()
-    parser.add_argument('--protocol-externs-file')
-    args, _ = parser.parse_known_args()
-    if args.protocol_externs_file:
-        protocol_externs_file = args.protocol_externs_file
-    else:
-        generate_protocol_externs.generate_protocol_externs(protocol_externs_file, BROWSER_PROTOCOL_PATH, JS_PROTOCOL_PATH)
-    loader = modular_build.DescriptorLoader(DEVTOOLS_FRONTEND_PATH)
-    descriptors = loader.load_applications(APPLICATION_DESCRIPTORS)
-    modules_by_name = descriptors.modules
-
-    java_exec = find_java()
-    errors_found |= check_conditional_dependencies(modules_by_name)
-
-    print 'Compiling frontend...'
-    temp_devtools_path = tempfile.mkdtemp()
-    namespace_externs_path = generate_namespace_externs(modules_by_name)
-    compiler_args_file_path = prepare_closure_frontend_compile(temp_devtools_path, descriptors, namespace_externs_path,
-                                                               protocol_externs_file)
-    frontend_compile_proc = popen(
-        java_exec + ['-jar', CLOSURE_RUNNER_JAR, '--compiler-args-file',
-                     to_platform_path_exact(compiler_args_file_path)])
-
-    print 'Compiling devtools_compatibility.js...'
-
-    closure_compiler_command = java_exec + ['-jar', CLOSURE_COMPILER_JAR] + common_closure_args
-
-    devtools_js_compile_command = closure_compiler_command + [
-        '--externs',
-        to_platform_path(GLOBAL_EXTERNS_FILE), '--jscomp_off=externsValidation', '--js',
-        to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'devtools_compatibility.js'))
-    ]
-    devtools_js_compile_proc = popen(devtools_js_compile_command)
-
-    errors_found |= JSDocChecker(descriptors, java_exec).check()
-
-    (devtools_js_compile_out, _) = devtools_js_compile_proc.communicate()
-    print 'devtools_compatibility.js compilation output:%s' % os.linesep, devtools_js_compile_out
-    errors_found |= has_errors(devtools_js_compile_out)
-
-    (frontend_compile_out, _) = frontend_compile_proc.communicate()
-    print 'devtools frontend compilation output:'
-    for line in frontend_compile_out.splitlines():
-        if "@@ START_MODULE" in line or "@@ END_MODULE" in line:
-            continue
-        print line
-    errors_found |= has_errors(frontend_compile_out)
-
-    os.remove(protocol_externs_file)
-    os.remove(namespace_externs_path)
-    os.remove(compiler_args_file_path)
-    shutil.rmtree(temp_devtools_path, True)
-
-    if errors_found:
-        print 'ERRORS DETECTED'
-        sys.exit(1)
-    print 'DONE - compiled without errors'
-
-
-if __name__ == "__main__":
-    main()
+run_type_check.main()
diff --git a/scripts/devtools_paths.py b/scripts/devtools_paths.py
index 6a50a6d..3ab1861 100644
--- a/scripts/devtools_paths.py
+++ b/scripts/devtools_paths.py
@@ -45,11 +45,12 @@
     return node.GetBinaryPath()
 
 
-DEVTOOLS_ROOT_PATH = path.join(path.dirname(__file__), '..')
+def devtools_root_path():
+    return path.dirname((path.dirname(path.abspath(__file__))))
 
 
 def node_modules_path():
-    return path.join(DEVTOOLS_ROOT_PATH, 'node_modules')
+    return path.join(devtools_root_path(), 'node_modules')
 
 
 def eslint_path():
@@ -61,4 +62,4 @@
 
 
 def package_json_path():
-    return path.join(DEVTOOLS_ROOT_PATH, 'package.json')
+    return path.join(devtools_root_path(), 'package.json')
diff --git a/scripts/extract_module/extract_module.js b/scripts/extract_module/extract_module.js
index 14a80e3..99b0867 100644
--- a/scripts/extract_module/extract_module.js
+++ b/scripts/extract_module/extract_module.js
@@ -9,7 +9,7 @@
 
 const FRONTEND_PATH = path.resolve(__dirname, '..', '..', 'front_end');
 const BUILD_GN_PATH = path.resolve(__dirname, '..', '..', 'BUILD.gn');
-const SPECIAL_CASE_NAMESPACES_PATH = path.resolve(__dirname, '..', 'special_case_namespaces.json');
+const SPECIAL_CASE_NAMESPACES_PATH = path.resolve(__dirname, '..', 'build', 'special_case_namespaces.json');
 
 /*
  * This is used to extract a new module from an existing module by:
diff --git a/scripts/lint_javascript.py b/scripts/lint_javascript.py
index 19d31cd..c67e733 100755
--- a/scripts/lint_javascript.py
+++ b/scripts/lint_javascript.py
@@ -4,87 +4,6 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import os.path as path
-import re
-import subprocess
-import sys
+from test import run_lint_check
 
-import devtools_paths
-
-files_to_lint = None
-
-if len(sys.argv) >= 2:
-    if sys.argv[1] == "--help":
-        print("Usage: %s [file|dir|glob]*" % path.basename(sys.argv[0]))
-        print
-        print(" [file|dir|glob]*  Path or glob to run eslint on.")
-        print("                   If absent, the entire frontend will be checked.")
-        sys.exit(0)
-
-    else:
-        print("Linting only these files:\n %s" % sys.argv[1:])
-        files_to_lint = sys.argv[1:]
-
-is_cygwin = sys.platform == "cygwin"
-
-
-def popen(arguments, cwd=None):
-    return subprocess.Popen(arguments, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-
-
-def to_platform_path(filepath):
-    if not is_cygwin:
-        return filepath
-    return re.sub(r"^/cygdrive/(\w)", "\\1:", filepath)
-
-
-def to_platform_path_exact(filepath):
-    if not is_cygwin:
-        return filepath
-    output, _ = popen(["cygpath", "-w", filepath]).communicate()
-    # pylint: disable=E1103
-    return output.strip().replace("\\", "\\\\")
-
-
-scripts_path = path.dirname(path.abspath(__file__))
-devtools_path = path.dirname(scripts_path)
-devtools_frontend_path = path.join(devtools_path, "front_end")
-
-print("Linting JavaScript with eslint...\n")
-
-
-def js_lint(files_list=None):
-    eslint_errors_found = False
-
-    if files_list is None:
-        files_list = [devtools_frontend_path]
-    files_list = [file_name for file_name in files_list if not file_name.endswith(".eslintrc.js")]
-
-    eslintconfig_path = path.join(devtools_path, ".eslintrc.js")
-    eslintignore_path = path.join(devtools_path, ".eslintignore")
-    exec_command = [
-        devtools_paths.node_path(),
-        devtools_paths.eslint_path(),
-        "--config",
-        to_platform_path_exact(eslintconfig_path),
-        "--ignore-path",
-        to_platform_path_exact(eslintignore_path),
-        "--fix",
-    ] + files_list
-
-    eslint_proc = popen(exec_command, cwd=devtools_path)
-    (eslint_proc_out, _) = eslint_proc.communicate()
-    if eslint_proc.returncode != 0:
-        eslint_errors_found = True
-    else:
-        print("eslint exited successfully")
-
-    print(eslint_proc_out)
-    return eslint_errors_found
-
-
-errors_found = js_lint(files_to_lint)
-
-if errors_found:
-    print("ERRORS DETECTED")
-    sys.exit(1)
+run_lint_check.main()
diff --git a/scripts/migration/refactor-to-es-module.ts b/scripts/migration/refactor-to-es-module.ts
index 3b1406c..8996ca2 100644
--- a/scripts/migration/refactor-to-es-module.ts
+++ b/scripts/migration/refactor-to-es-module.ts
@@ -148,7 +148,7 @@
   return print(ast).code;
 }
 
-const FOLDER_MAPPING: {[name: string]: string} = require(path.join('..', 'special_case_namespaces.json'));
+const FOLDER_MAPPING: {[name: string]: string} = require(path.join('..', 'build', 'special_case_namespaces.json'));
 
 function computeNamespaceName(folderName: string): string {
   if (folderName in FOLDER_MAPPING) {
@@ -173,4 +173,4 @@
   process.exit(1);
 }
 
-main(process.argv[2], process.argv[3]);
\ No newline at end of file
+main(process.argv[2], process.argv[3]);
diff --git a/scripts/run_tests.py b/scripts/run_tests.py
index 393b5c3..9755526 100755
--- a/scripts/run_tests.py
+++ b/scripts/run_tests.py
@@ -3,81 +3,7 @@
 # Copyright 2019 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.
-"""
-Run Karma unit tests on a pre-built chrome or one specified via --chrome-binary.
-"""
 
-import os
-import re
-import subprocess
-import sys
+from test import run_unittests
 
-import devtools_paths
-
-
-def check_chrome_binary(chrome_binary):
-    return os.path.exists(chrome_binary) and os.path.isfile(chrome_binary) and os.access(chrome_binary, os.X_OK)
-
-
-def popen(arguments, cwd=None, env=None):
-    return subprocess.Popen(arguments, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
-
-
-def to_platform_path_exact(filepath):
-    if not is_cygwin:
-        return filepath
-    output, _ = popen(['cygpath', '-w', filepath]).communicate()
-    # pylint: disable=E1103
-    return output.strip().replace('\\', '\\\\')
-
-
-def run_tests():
-    karma_errors_found = False
-
-    karmaconfig_path = os.path.join(devtools_path, 'karma.conf.js')
-    exec_command = [devtools_paths.node_path(), devtools_paths.karma_path(), 'start', to_platform_path_exact(karmaconfig_path)]
-
-    env = {'NODE_PATH': devtools_paths.node_modules_path()}
-    if (chrome_binary is not None):
-        env['CHROME_BIN'] = chrome_binary
-
-    karma_proc = popen(exec_command, cwd=devtools_path, env=env)
-
-    (karma_proc_out, _) = karma_proc.communicate()
-    if karma_proc.returncode != 0:
-        karma_errors_found = True
-    else:
-        print('Karma exited successfully')
-
-    print(karma_proc_out)
-    return karma_errors_found
-
-
-is_cygwin = sys.platform == "cygwin"
-chrome_binary = None
-DOWNLOADED_CHROME_BINARY = os.path.abspath(
-    os.path.join(os.path.dirname(__file__), '..', 'third_party', 'chrome', 'chrome-linux', 'chrome'))
-
-if check_chrome_binary(DOWNLOADED_CHROME_BINARY):
-    chrome_binary = DOWNLOADED_CHROME_BINARY
-
-if len(sys.argv) >= 2:
-    chrome_binary = re.sub(r"^\-\-chrome-binary=(.*)", "\\1", sys.argv[1])
-    if not check_chrome_binary(chrome_binary):
-        print("Unable to find a Chrome binary at \"%s\"" % chrome_binary)
-        sys.exit(1)
-
-scripts_path = os.path.dirname(os.path.abspath(__file__))
-devtools_path = os.path.dirname(scripts_path)
-
-print("Running tests with Karma...")
-if (chrome_binary is not None):
-    print("Using custom Chrome Binary (%s)\n" % chrome_binary)
-else:
-    print("Using system Chrome")
-
-errors_found = run_tests()
-
-if errors_found:
-    print('ERRORS DETECTED')
-    sys.exit(1)
+run_unittests.main()
diff --git a/scripts/run_web_tests.py b/scripts/run_web_tests.py
deleted file mode 100755
index 3a9f091..0000000
--- a/scripts/run_web_tests.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/python
-
-# Copyright 2019 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.
-"""
-Integrate into Chromium and run webtests.
-"""
-
-import argparse
-import os
-import shutil
-import subprocess
-import sys
-
-
-def parse_options(cli_args):
-    parser = argparse.ArgumentParser(description='Integrate into Chromium and run webtests.')
-    parser.add_argument('chromium_dir', help='Chromium directory')
-    parser.add_argument('devtools_dir', help='DevTools directory')
-    parser.add_argument('--nopatch', help='skip patching', action='store_true')
-    parser.add_argument('--nobuild', help='skip building', action='store_true')
-    return parser.parse_args(cli_args)
-
-
-def patch(options):
-    subprocess.check_call(['git', 'fetch', 'origin'], cwd=options.chromium_dir)
-    subprocess.check_call(['git', 'checkout', 'origin/lkgr'], cwd=options.chromium_dir)
-    DEVTOOLS_PATH = os.path.abspath(options.devtools_dir)
-    PATCH_FILE = os.path.join(DEVTOOLS_PATH, 'scripts', 'chromium.patch')
-    subprocess.check_call(['git', 'apply', PATCH_FILE], cwd=options.chromium_dir)
-    subprocess.check_call(['gclient', 'setdep', '--var=devtools_frontend_url=file://%s' % DEVTOOLS_PATH], cwd=options.chromium_dir)
-    subprocess.check_call(['gclient', 'setdep', '--var=devtools_frontend_revision=FETCH_HEAD'], cwd=options.chromium_dir)
-    subprocess.check_call(['gclient', 'sync'], cwd=options.chromium_dir)
-
-
-def build(options):
-    subprocess.check_call(['autoninja', '-C', 'out/Release', 'blink_tests'], cwd=options.chromium_dir)
-
-
-def run(options):
-    subprocess.check_call(
-        [os.path.join(options.chromium_dir, 'third_party', 'blink', 'tools', 'run_web_tests.py'), 'http/tests/devtools/'])
-
-
-if __name__ == '__main__':
-    OPTIONS = parse_options(sys.argv[1:])
-    if not OPTIONS.nopatch:
-        patch(OPTIONS)
-    if not OPTIONS.nobuild:
-        build(OPTIONS)
-    run(OPTIONS)
diff --git a/scripts/test/__init__.py b/scripts/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/scripts/test/__init__.py
diff --git a/scripts/test/run_lint_check.py b/scripts/test/run_lint_check.py
new file mode 100755
index 0000000..e4f96ca
--- /dev/null
+++ b/scripts/test/run_lint_check.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 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.
+
+import os.path as path
+import re
+import subprocess
+import sys
+
+scripts_path = path.dirname(path.dirname(path.abspath(__file__)))
+sys.path.append(scripts_path)
+import devtools_paths
+
+files_to_lint = None
+
+if len(sys.argv) >= 2:
+    if sys.argv[1] == '--help':
+        print('Usage: %s [file|dir|glob]*' % path.basename(sys.argv[0]))
+        print
+        print(' [file|dir|glob]*  Path or glob to run eslint on.')
+        print('                   If absent, the entire frontend will be checked.')
+        sys.exit(0)
+
+    else:
+        print('Linting only these files:\n %s' % sys.argv[1:])
+        files_to_lint = sys.argv[1:]
+
+is_cygwin = sys.platform == 'cygwin'
+
+
+def popen(arguments, cwd=None):
+    return subprocess.Popen(arguments, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+
+def to_platform_path(filepath):
+    if not is_cygwin:
+        return filepath
+    return re.sub(r'^/cygdrive/(\w)', '\\1:', filepath)
+
+
+def to_platform_path_exact(filepath):
+    if not is_cygwin:
+        return filepath
+    output, _ = popen(['cygpath', '-w', filepath]).communicate()
+    # pylint: disable=E1103
+    return output.strip().replace('\\', '\\\\')
+
+
+devtools_path = devtools_paths.devtools_root_path()
+devtools_frontend_path = path.join(devtools_path, 'front_end')
+
+print('Linting JavaScript with eslint...\n')
+
+
+def js_lint(files_list=None):
+    eslint_errors_found = False
+
+    if files_list is None:
+        files_list = [devtools_frontend_path]
+    files_list = [file_name for file_name in files_list if not file_name.endswith('.eslintrc.js')]
+
+    eslintconfig_path = path.join(devtools_path, '.eslintrc.js')
+    eslintignore_path = path.join(devtools_path, '.eslintignore')
+    exec_command = [
+        devtools_paths.node_path(),
+        devtools_paths.eslint_path(),
+        '--config',
+        to_platform_path_exact(eslintconfig_path),
+        '--ignore-path',
+        to_platform_path_exact(eslintignore_path),
+        '--fix',
+    ] + files_list
+
+    eslint_proc = popen(exec_command, cwd=devtools_path)
+    (eslint_proc_out, _) = eslint_proc.communicate()
+    if eslint_proc.returncode != 0:
+        eslint_errors_found = True
+    else:
+        print('eslint exited successfully')
+
+    print(eslint_proc_out)
+    return eslint_errors_found
+
+
+def main():
+    errors_found = js_lint(files_to_lint)
+
+    if errors_found:
+        print('ERRORS DETECTED')
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/test/run_type_check.py b/scripts/test/run_type_check.py
new file mode 100755
index 0000000..499898d
--- /dev/null
+++ b/scripts/test/run_type_check.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import argparse
+import os
+import os.path as path
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+SCRIPTS_PATH = path.dirname(path.dirname(path.abspath(__file__)))
+sys.path.append(SCRIPTS_PATH)
+
+from build import dependency_preprocessor
+from build import generate_protocol_externs
+from build import modular_build
+from build import special_case_namespaces
+
+import devtools_paths
+import utils
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+is_cygwin = sys.platform == 'cygwin'
+
+
+def popen(arguments):
+    return subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+
+def to_platform_path(filepath):
+    if not is_cygwin:
+        return filepath
+    return re.sub(r'^/cygdrive/(\w)', '\\1:', filepath)
+
+
+def to_platform_path_exact(filepath):
+    if not is_cygwin:
+        return filepath
+    output, _ = popen(['cygpath', '-w', filepath]).communicate()
+    # pylint: disable=E1103
+    return output.strip().replace('\\', '\\\\')
+
+
+DEVTOOLS_PATH = devtools_paths.devtools_root_path()
+SCRIPTS_PATH = path.join(DEVTOOLS_PATH, 'scripts')
+ROOT_PATH = devtools_paths.root_path()
+BROWSER_PROTOCOL_PATH = path.join(ROOT_PATH, 'third_party', 'blink', 'renderer', 'core', 'inspector', 'browser_protocol.pdl')
+# TODO(dgozman): move these checks to v8.
+JS_PROTOCOL_PATH = path.join(ROOT_PATH, 'v8', 'include', 'js_protocol.pdl')
+DEVTOOLS_FRONTEND_PATH = path.join(DEVTOOLS_PATH, 'front_end')
+GLOBAL_EXTERNS_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'externs.js'))
+DEFAULT_PROTOCOL_EXTERNS_FILE = path.join(DEVTOOLS_FRONTEND_PATH, 'protocol_externs.js')
+RUNTIME_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'Runtime.js'))
+ROOT_MODULE_FILE = to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'root.js'))
+
+CLOSURE_COMPILER_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'closure', 'compiler.jar'))
+CLOSURE_RUNNER_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'closure', 'closure_runner', 'closure_runner.jar'))
+JSDOC_VALIDATOR_JAR = to_platform_path(path.join(SCRIPTS_PATH, 'jsdoc_validator', 'jsdoc_validator.jar'))
+
+TYPE_CHECKED_JSDOC_TAGS_LIST = ['param', 'return', 'type', 'enum']
+TYPE_CHECKED_JSDOC_TAGS_OR = '|'.join(TYPE_CHECKED_JSDOC_TAGS_LIST)
+
+# Basic regex for invalid JsDoc types: an object type name ([A-Z][_A-Za-z0-9.]+[A-Za-z0-9]) not preceded by '!', '?', ':' (this, new), or '.' (object property).
+INVALID_TYPE_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR +
+                                r')\s*\{.*(?<![!?:._A-Za-z0-9])([A-Z][_A-Za-z0-9.]+[A-Za-z0-9])[^/]*\}')
+INVALID_TYPE_DESIGNATOR_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR + r')\s*.*(?<![{: ])([?!])=?\}')
+INVALID_NON_OBJECT_TYPE_REGEX = re.compile(r'@(?:' + TYPE_CHECKED_JSDOC_TAGS_OR + r')\s*\{.*(![a-z]+)[^/]*\}')
+ERROR_WARNING_REGEX = re.compile(r'WARNING|ERROR')
+LOADED_CSS_REGEX = re.compile(r'(?:registerRequiredCSS|WebInspector\.View\.createStyleElement)\s*\(\s*"(.+)"\s*\)')
+
+JAVA_BUILD_REGEX = re.compile(r'\w+ version "(\d+)\.(\d+)')
+
+
+def log_error(message):
+    print 'ERROR: ' + message
+
+
+def error_excepthook(exctype, value, traceback):
+    print 'ERROR:'
+    sys.__excepthook__(exctype, value, traceback)
+
+
+sys.excepthook = error_excepthook
+
+APPLICATION_DESCRIPTORS = [
+    'inspector',
+    'toolbox',
+    'integration_test_runner',
+    'formatter_worker',
+    'heap_snapshot_worker',
+]
+
+SKIPPED_NAMESPACES = {
+    'Console',  # Closure uses Console as a namespace item so we cannot override it right now.
+    'Gonzales',  # third party module defined in front_end/externs.js
+    'Terminal',  # third party module defined in front_end/externs.js
+}
+
+
+def has_errors(output):
+    return re.search(ERROR_WARNING_REGEX, output) is not None
+
+
+class JSDocChecker:
+
+    def __init__(self, descriptors, java_exec):
+        self._error_found = False
+        self._all_files = descriptors.all_compiled_files()
+        self._java_exec = java_exec
+
+    def check(self):
+        print 'Verifying JSDoc comments...'
+        self._verify_jsdoc()
+        self._run_jsdoc_validator()
+        return self._error_found
+
+    def _run_jsdoc_validator(self):
+        files = [to_platform_path(f) for f in self._all_files]
+        file_list = tempfile.NamedTemporaryFile(mode='wt', delete=False)
+        try:
+            file_list.write('\n'.join(files))
+        finally:
+            file_list.close()
+        proc = popen(self._java_exec + ['-jar', JSDOC_VALIDATOR_JAR, '--files-list-name', to_platform_path_exact(file_list.name)])
+        (out, _) = proc.communicate()
+        if out:
+            print('JSDoc validator output:%s%s' % (os.linesep, out))
+            self._error_found = True
+        os.remove(file_list.name)
+
+    def _verify_jsdoc(self):
+        for full_file_name in self._all_files:
+            line_index = 0
+            with open(full_file_name, 'r') as sourceFile:
+                for line in sourceFile:
+                    line_index += 1
+                    if line.rstrip():
+                        self._verify_jsdoc_line(full_file_name, line_index, line)
+
+    def _verify_jsdoc_line(self, file_name, line_index, line):
+
+        def print_error(message, error_position):
+            print '%s:%s: ERROR - %s%s%s%s%s%s' % (file_name, line_index, message, os.linesep, line, os.linesep,
+                                                   ' ' * error_position + '^', os.linesep)
+
+        known_css = {}
+        match = re.search(INVALID_TYPE_REGEX, line)
+        if match:
+            print_error('Type "%s" nullability not marked explicitly with "?" (nullable) or "!" (non-nullable)' % match.group(1),
+                        match.start(1))
+            self._error_found = True
+
+        match = re.search(INVALID_NON_OBJECT_TYPE_REGEX, line)
+        if match:
+            print_error('Non-object type explicitly marked with "!" (non-nullable), which is the default and should be omitted',
+                        match.start(1))
+            self._error_found = True
+
+        match = re.search(INVALID_TYPE_DESIGNATOR_REGEX, line)
+        if match:
+            print_error('Type nullability indicator misplaced, should precede type', match.start(1))
+            self._error_found = True
+
+        match = re.search(LOADED_CSS_REGEX, line)
+        if match:
+            css_file = path.join(DEVTOOLS_FRONTEND_PATH, match.group(1))
+            exists = known_css.get(css_file)
+            if exists is None:
+                exists = path.isfile(css_file)
+                known_css[css_file] = exists
+            if not exists:
+                print_error('Dynamically loaded CSS stylesheet is missing in the source tree', match.start(1))
+                self._error_found = True
+
+
+def find_java():
+    required_major = 1
+    required_minor = 7
+    exec_command = None
+    has_server_jvm = True
+    java_path = utils.which('java')
+
+    if not java_path:
+        print 'NOTE: No Java executable found in $PATH.'
+        sys.exit(1)
+
+    is_ok = False
+    java_version_out, _ = popen([java_path, '-version']).communicate()
+    # pylint: disable=E1103
+    match = re.search(JAVA_BUILD_REGEX, java_version_out)
+    if match:
+        major = int(match.group(1))
+        minor = int(match.group(2))
+        is_ok = major > required_major or major == required_major and minor >= required_minor
+    if is_ok:
+        exec_command = [java_path, '-Xms1024m', '-server', '-XX:+TieredCompilation']
+        check_server_proc = popen(exec_command + ['-version'])
+        check_server_proc.communicate()
+        if check_server_proc.returncode != 0:
+            # Not all Java installs have server JVMs.
+            exec_command = exec_command.remove('-server')
+            has_server_jvm = False
+
+    if not is_ok:
+        print 'NOTE: Java executable version %d.%d or above not found in $PATH.' % (required_major, required_minor)
+        sys.exit(1)
+    print 'Java executable: %s%s' % (java_path, '' if has_server_jvm else ' (no server JVM)')
+    return exec_command
+
+
+common_closure_args = [
+    '--summary_detail_level',
+    '3',
+    '--jscomp_error',
+    'visibility',
+    '--jscomp_warning',
+    'missingOverride',
+    '--compilation_level',
+    'SIMPLE_OPTIMIZATIONS',
+    '--warning_level',
+    'VERBOSE',
+    '--language_in=ECMASCRIPT_NEXT',
+    '--language_out=ES5_STRICT',
+    '--extra_annotation_name',
+    'suppressReceiverCheck',
+    '--extra_annotation_name',
+    'suppressGlobalPropertiesCheck',
+    '--checks-only',
+]
+
+GENERATED_SKIP_COMPILATION_FILES = [
+    'SupportedCSSProperties.js',
+    'InspectorBackendCommands.js',
+]
+
+
+def check_conditional_dependencies(modules_by_name):
+    errors_found = False
+    for name in modules_by_name:
+        if 'test_runner' in name:
+            continue
+        for dep_name in modules_by_name[name].get('dependencies', []):
+            dependency = modules_by_name[dep_name]
+            if dependency.get('experiment') or dependency.get('condition'):
+                log_error('Module "%s" may not depend on the conditional module "%s"' % (name, dep_name))
+                errors_found = True
+    return errors_found
+
+
+def prepare_closure_frontend_compile(temp_devtools_path, descriptors, namespace_externs_path, protocol_externs_file):
+    temp_frontend_path = path.join(temp_devtools_path, 'front_end')
+    checker = dependency_preprocessor.DependencyPreprocessor(descriptors, temp_frontend_path, DEVTOOLS_FRONTEND_PATH)
+    checker.enforce_dependencies()
+
+    command = common_closure_args + [
+        '--externs',
+        to_platform_path(GLOBAL_EXTERNS_FILE),
+        '--externs',
+        namespace_externs_path,
+        '--js',
+        RUNTIME_FILE,
+        '--js',
+        ROOT_MODULE_FILE,
+    ]
+
+    all_files = descriptors.all_compiled_files()
+    args = []
+    for file in all_files:
+        args.extend(['--js', file])
+        if 'InspectorBackend.js' in file:
+            args.extend(['--js', protocol_externs_file])
+
+    for file in GENERATED_SKIP_COMPILATION_FILES:
+        # Write a dummy file for skipped compilation files that are autogenerated.
+        # We don't type-check this file, but we import them via ES modules
+        generated_file = path.join(temp_frontend_path, file)
+        modular_build.write_file(generated_file, '')
+        args.extend(['--js', generated_file])
+
+    command += args
+    command = [arg.replace(DEVTOOLS_FRONTEND_PATH, temp_frontend_path) for arg in command]
+    compiler_args_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
+    try:
+        compiler_args_file.write('devtools_frontend %s' % (' '.join(command)))
+    finally:
+        compiler_args_file.close()
+    return compiler_args_file.name
+
+
+def generate_namespace_externs(modules_by_name):
+
+    def map_module_to_namespace(module):
+        return special_case_namespaces.special_case_namespaces.get(module, to_camel_case(module))
+
+    def to_camel_case(snake_string):
+        components = snake_string.split('_')
+        return ''.join(x.title() for x in components)
+
+    all_namespaces = [map_module_to_namespace(module) for module in modules_by_name]
+    namespaces = [namespace for namespace in all_namespaces if namespace not in SKIPPED_NAMESPACES]
+    namespaces.sort()
+    namespace_externs_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
+    try:
+        namespace_externs_file.write('var Root = {};\n')
+        for namespace in namespaces:
+            namespace_externs_file.write('var %s = {};\n' % namespace)
+    finally:
+        namespace_externs_file.close()
+    namespace_externs_path = to_platform_path(namespace_externs_file.name)
+    return namespace_externs_path
+
+
+def main():
+    protocol_externs_file = DEFAULT_PROTOCOL_EXTERNS_FILE
+    errors_found = False
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--protocol-externs-file')
+    args, _ = parser.parse_known_args()
+    if args.protocol_externs_file:
+        protocol_externs_file = args.protocol_externs_file
+    else:
+        generate_protocol_externs.generate_protocol_externs(protocol_externs_file, BROWSER_PROTOCOL_PATH, JS_PROTOCOL_PATH)
+    loader = modular_build.DescriptorLoader(DEVTOOLS_FRONTEND_PATH)
+    descriptors = loader.load_applications(APPLICATION_DESCRIPTORS)
+    modules_by_name = descriptors.modules
+
+    java_exec = find_java()
+    errors_found |= check_conditional_dependencies(modules_by_name)
+
+    print 'Compiling frontend...'
+    temp_devtools_path = tempfile.mkdtemp()
+    namespace_externs_path = generate_namespace_externs(modules_by_name)
+    compiler_args_file_path = prepare_closure_frontend_compile(temp_devtools_path, descriptors, namespace_externs_path,
+                                                               protocol_externs_file)
+    frontend_compile_proc = popen(
+        java_exec + ['-jar', CLOSURE_RUNNER_JAR, '--compiler-args-file',
+                     to_platform_path_exact(compiler_args_file_path)])
+
+    print 'Compiling devtools_compatibility.js...'
+
+    closure_compiler_command = java_exec + ['-jar', CLOSURE_COMPILER_JAR] + common_closure_args
+
+    devtools_js_compile_command = closure_compiler_command + [
+        '--externs',
+        to_platform_path(GLOBAL_EXTERNS_FILE), '--jscomp_off=externsValidation', '--js',
+        to_platform_path(path.join(DEVTOOLS_FRONTEND_PATH, 'devtools_compatibility.js'))
+    ]
+    devtools_js_compile_proc = popen(devtools_js_compile_command)
+
+    errors_found |= JSDocChecker(descriptors, java_exec).check()
+
+    (devtools_js_compile_out, _) = devtools_js_compile_proc.communicate()
+    print 'devtools_compatibility.js compilation output:%s' % os.linesep, devtools_js_compile_out
+    errors_found |= has_errors(devtools_js_compile_out)
+
+    (frontend_compile_out, _) = frontend_compile_proc.communicate()
+    print 'devtools frontend compilation output:'
+    for line in frontend_compile_out.splitlines():
+        if '@@ START_MODULE' in line or '@@ END_MODULE' in line:
+            continue
+        print line
+    errors_found |= has_errors(frontend_compile_out)
+
+    os.remove(protocol_externs_file)
+    os.remove(namespace_externs_path)
+    os.remove(compiler_args_file_path)
+    shutil.rmtree(temp_devtools_path, True)
+
+    if errors_found:
+        print 'ERRORS DETECTED'
+        sys.exit(1)
+    print 'DONE - compiled without errors'
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/test/run_unittests.py b/scripts/test/run_unittests.py
new file mode 100755
index 0000000..6aa0cc2
--- /dev/null
+++ b/scripts/test/run_unittests.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+#
+# Copyright 2019 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.
+"""
+Run Karma unit tests on a pre-built chrome or one specified via --chrome-binary.
+"""
+
+import os
+import re
+import subprocess
+import sys
+
+scripts_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(scripts_path)
+
+import devtools_paths
+
+
+def check_chrome_binary(chrome_binary):
+    return os.path.exists(chrome_binary) and os.path.isfile(chrome_binary) and os.access(chrome_binary, os.X_OK)
+
+
+def popen(arguments, cwd=None, env=None):
+    return subprocess.Popen(arguments, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
+
+
+def to_platform_path_exact(filepath):
+    if not is_cygwin:
+        return filepath
+    output, _ = popen(['cygpath', '-w', filepath]).communicate()
+    # pylint: disable=E1103
+    return output.strip().replace('\\', '\\\\')
+
+
+def run_tests():
+    karma_errors_found = False
+    karmaconfig_path = os.path.join(devtools_path, 'karma.conf.js')
+    exec_command = [devtools_paths.node_path(), devtools_paths.karma_path(), 'start', to_platform_path_exact(karmaconfig_path)]
+
+    env = {'NODE_PATH': devtools_paths.node_modules_path()}
+    if (chrome_binary is not None):
+        env['CHROME_BIN'] = chrome_binary
+
+    karma_proc = popen(exec_command, cwd=devtools_path, env=env)
+
+    (karma_proc_out, _) = karma_proc.communicate()
+    if karma_proc.returncode != 0:
+        karma_errors_found = True
+    else:
+        print('Karma exited successfully')
+
+    print(karma_proc_out)
+    return karma_errors_found
+
+
+devtools_path = devtools_paths.devtools_root_path()
+is_cygwin = sys.platform == 'cygwin'
+chrome_binary = None
+DOWNLOADED_CHROME_BINARY = os.path.abspath(os.path.join(devtools_path, 'third_party', 'chrome', 'chrome-linux', 'chrome'))
+
+if check_chrome_binary(DOWNLOADED_CHROME_BINARY):
+    chrome_binary = DOWNLOADED_CHROME_BINARY
+
+if len(sys.argv) >= 2:
+    chrome_binary = re.sub(r'^\-\-chrome-binary=(.*)', '\\1', sys.argv[1])
+    if not check_chrome_binary(chrome_binary):
+        print('Unable to find a Chrome binary at \'%s\'' % chrome_binary)
+        sys.exit(1)
+print('Running tests with Karma...')
+if (chrome_binary is not None):
+    print('Using custom Chrome Binary (%s)\n' % chrome_binary)
+else:
+    print('Using system Chrome')
+
+
+def main():
+    errors_found = run_tests()
+
+    if errors_found:
+        print('ERRORS DETECTED')
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()