chromium/tools/android/generate_java_test/generate_java_test.py

#!/usr/bin/env python
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''
This script helps to generate Java instrumentation tests and/or Unit test
(robolectric) for new feature Other than that, it helps to add your source
file (foo.java) and test files (footest.java) to gn (java_sources.gni)
Also, it will generate OWNER file with your [email protected] if it doesn't exit

Where to run?
Anywhere in the repo as the tool can find the abstract paths.

How to run? sample:
If you are building the following example like
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"

Use command:
python generate_java_test.py --instrumentation --unittest --source \
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
   -i, --instrumentation   generate instrumentation test file
   -u, --unittest          generate unittest file
   Default will generate both of them
   -s, --source            source file with path [must have]

What do you get?
- test files created
- gn file updated
- OWNER created

What do you need to do?
- Write TESTs!


'''
from __future__ import print_function

import argparse
import datetime
import os
import re
import bisect

# Below sessions are contents for test files
this_year = str(datetime.datetime.now().year)

_INST_TEST_FILE = '''// Copyright %s The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// generate_java_test.py

package %s;

import org.junit.runner.RunWith;

import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.ui.test.util.BlankUiTestActivityTestCase;

/** Instrumentation tests for {@link %s}. */
@RunWith(ChromeJUnit4ClassRunner.class)
public class %sInstrumentationTest extends BlankUiTestActivityTestCase {

}
'''

_UNIT_TEST_FILE = '''// Copyright %s The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// generate_java_test.py

package %s;

import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;

/** Unit tests for {@link %s}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class %sTest {

}

'''

# End or contents

# Set root to be "src/" like src/tools/android/gn_java_test/../../../
root_path = os.path.abspath(os.path.dirname(__file__)) + "/../../../"

# path of gni files
javatest_gni = os.path.join(root_path,
                            "chrome/android/chrome_test_java_sources.gni")
junittest_gni = os.path.join(
    root_path, "chrome/android/chrome_junit_test_java_sources.gni")

# Help message if the user use wrong arguments
HELP_MESSAGE = '''
Wrong --source pattern! Please use the sample below
generate_create_test.py --unittest --source \
chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java
'''


# used for better logging
class bcolors:
  HEADER = '\033[95m'
  OKBLUE = '\033[94m'
  OKGREEN = '\033[92m'
  WARNING = '\033[93m'
  FAIL = '\033[91m'
  ENDC = '\033[0m'
  BOLD = '\033[1m'
  UNDERLINE = '\033[4m'


def failmsg(s):
  return "%s%s%s" % (bcolors.FAIL, s, bcolors.ENDC)


def infomsg(s):
  return "%s%s%s" % (bcolors.OKBLUE, s, bcolors.ENDC)


def warningmsg(s):
  return "%s%s%s" % (bcolors.WARNING, s, bcolors.ENDC)


# -----------------  APIs and main function are below ----------------------#


# Verify if source file exist, return () if it doesn't exist,
# otherwise, return (package_name, file_name)
def GetPackageAndFile(source):
  if not os.path.exists(os.path.join(root_path, source)):
    print(failmsg("%s%s Does not exist!" % (root_path, source)))
    return ()
#TODO(yzjr): Will add support for components under chrome/android/features
#crbug/950783
  matchSource = re.match(
      "chrome\/android\/java\/src\/" +
      "(org\/chromium\/chrome\/browser\/.*\/?)\/(.*)\.java", source)

  if not matchSource:
    print(failmsg(HELP_MESSAGE))
    return ()

  package_name = matchSource.group(1)
  print(infomsg("Package name: %s" % package_name))
  file_name = matchSource.group(2)
  print(infomsg("File name: %s" % file_name))
  return (package_name, file_name)

# Generate an instrumentation test file, OWNER and modify corresponding GNI
def CreateInstrumentationTestFile(package_path, file_name):
  package_name = package_path.replace('/', '.')

  ins_testfile_path = os.path.join(root_path, 'chrome/android/javatests/src/',
                                   package_path)
  ins_testfile = os.path.join(ins_testfile_path, (file_name +
    'InstrumentationTest.java'))
  print(infomsg("+++ Creating instrumentation_test file: %s" % ins_testfile))

  if os.path.exists(ins_testfile):
    print(warningmsg("%s already exist!\n" % ins_testfile))
  else:
    dir_name = os.path.dirname(ins_testfile)
    if not os.path.exists(dir_name):
      os.makedirs(dir_name)
    try:
      file = open(ins_testfile, "w")
      filecontent = _INST_TEST_FILE % (this_year, package_name, file_name,
                                       file_name)
      file.write(filecontent)
      file.close()
    except Exception as e:
      print(warningmsg(e.message))
  # Create Owner file if source code OWNERS exists, otherwise skip
  source_ownerfile = os.path.join('chrome/android/java/src/', package_path,
                                  'OWNERS')
  if os.path.exists(os.path.join(root_path, source_ownerfile)):
    ins_owner = os.path.join(ins_testfile_path, 'OWNERS')
    CreateOwnerFile(ins_owner, source_ownerfile)
  # Modify GN file
  tag = "chrome_test_java_sources"
  txt = 'javatests/src/%s/%sInstrumentationTest.java' % (package_path,
      file_name)
  ModifyGnFile(javatest_gni, tag, txt)


# Generate a unit test file, OWNER and modify corresponding GNI
def CreateUnitTestFile(package_path, file_name):
  package_name = package_path.replace('/', '.')

  unit_testfile_path = os.path.join(root_path, 'chrome/android/junit/src/',
                                    package_path)
  unit_testfile = os.path.join(unit_testfile_path, file_name + "Test.java")
  print(infomsg("+++ Creating unit test file: %s" % unit_testfile))

  if os.path.exists(unit_testfile):
    print(warningmsg("%s already exist!\n" % unit_testfile))
  else:
    dir_name = os.path.dirname(unit_testfile)
    if not os.path.exists(dir_name):
      os.makedirs(dir_name)
    try:
      file = open(unit_testfile, "w")
      filecontent = _UNIT_TEST_FILE % (this_year, package_name, file_name,
                                       file_name)
      file.write(filecontent)
      file.close()
    except Exception as e:
      print(warningmsg(e.message))
  # Create Owner file if source code OWNERS exists, otherwise skip
  source_ownerfile = os.path.join('chrome/android/java/src/', package_path,
                                  'OWNERS')
  if os.path.exists(os.path.join(root_path, source_ownerfile)):
    unit_testowner = os.path.join(unit_testfile_path, "OWNERS")
    CreateOwnerFile(unit_testowner, source_ownerfile)
  # Modify GN file
  tag = "chrome_junit_test_java_sources"
  txt = 'junit/src/%s/%sTest.java' % (package_path, file_name)
  ModifyGnFile(junittest_gni, tag, txt)

# Create OWNER file if it doesn't exist
def CreateOwnerFile(ownerfile, source_owners):
  if os.path.exists(ownerfile):
    print(warningmsg("%s already exists!" % ownerfile))
    return
  try:
    file = open(ownerfile, "w")
    file.write("file://" + source_owners)
    file.close()
  except Exception as e:
    print(bcolors.WARNING + e.message + bcolors.ENDC)

# Modify GN file
def ModifyGnFile(gn_file, tag, txt):
  read_lines = []
  filelist = {}

  try:
    with open(gn_file) as f:
      code = compile(f.read(), "sometempfile.py", 'exec')
      exec (code, filelist)
      if tag not in filelist:
        print(warningmsg("%s is not found\n" % tag))
        return
      if txt in filelist[tag]:
        print(warningmsg("%s already exists\n" % txt))
        return

      # sorted the list as it might not be sorted from user input
      testfiles = sorted(filelist[tag])
      # insert test file to an sorted list
      bisect.insort(testfiles, txt)
    f.close()
    # read file headers with comments or others until tag
    with open(gn_file) as f:
      for line in f:
        stripped = line.strip()
        if stripped.startswith(tag):
          read_lines.append(line)
      for files in testfiles:
        read_lines.append("  \"%s\",\n" % files)
        # end of tag
      read_lines.append(']')
    f.close()
    with open(gn_file, 'w') as f:
      f.write(''.join(read_lines))
    f.close()
  except Exception as e:
    print(warningmsg("Failed to modify %s because %s" % (gn_file, e.message)))


# *******************  Below is main function ***********************
def main():
  parser = argparse.ArgumentParser()
  parser.add_argument(
      "-u", "--unittest", help="create unit tests", action="store_true")
  parser.add_argument(
      "-i",
      "--instrumentation",
      help="create java instrumentation tests",
      action="store_true")
  parser.add_argument(
      "-s",
      "--source",
      type=str,
      help="source of java code" +
      "like chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java",
      required=True)

  args = parser.parse_args()

  source_path = GetPackageAndFile(args.source or '')
  if not source_path:
    exit()

  package_path = source_path[0]
  file_name = source_path[1]

  # Add test files to java_sources.gni if they don't exist
  ran = False
  if args.unittest:
    ran = True
    CreateUnitTestFile(package_path, file_name)
  if args.instrumentation:
    ran = True
    CreateInstrumentationTestFile(package_path, file_name)
  if not ran:  # default for both tests
    CreateInstrumentationTestFile(package_path, file_name)
    CreateUnitTestFile(package_path, file_name)


if __name__ == "__main__":
  main()
'''

Test cases for this script:

* wrong commands *
python generate_java_test.py
python generate_java_test.py -h
python generate_java_test.py --instrumentation
python generate_java_test.py --unittest
python generate_java_test.py --instrumentation --unittest --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"

* good commands *
python generate_java_test.py --instrumentation --unittest --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
python generate_java_test.py --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
python generate_java_test.py --unittest --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
python generate_java_test.py --instrumentation --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"

* File exists - run twice *
python generate_java_test.py --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
python generate_java_test.py --source\
"chrome/android/java/src/org/chromium/chrome/browser/foo/Foo.java"
'''