chromium/components/cronet/tools/api_static_checks_unittest.py

#!/usr/bin/env python3
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""api_static_checks_unittest.py - Unittests for api_static_checks.py"""


import contextlib
import hashlib
import io
import os
import shutil
import sys
import tempfile
import unittest

REPOSITORY_ROOT = os.path.abspath(os.path.join(
    os.path.dirname(__file__), '..', '..', '..'))

sys.path.append(os.path.join(REPOSITORY_ROOT, 'components'))
from cronet.tools import api_static_checks  # pylint: disable=wrong-import-position
from cronet.tools import update_api  # pylint: disable=wrong-import-position

# pylint: disable=useless-object-inheritance


ERROR_PREFIX_CHECK_API_CALLS = (
"""ERROR: Found the following calls from implementation classes through
       API classes.  These could fail if older API is used that
       does not contain newer methods.  Please call through a
       wrapper class from VersionSafeCallbacks.
""")


ERROR_PREFIX_UPDATE_API = (
"""ERROR: This API was modified or removed:
             """)


ERROR_SUFFIX_UPDATE_API = (
"""

       Cronet API methods and classes cannot be modified.
""")


CHECK_API_VERSION_PREFIX = (
"""DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT

""")


API_FILENAME = './android/api.txt'
API_VERSION_FILENAME = './android/api_version.txt'


@contextlib.contextmanager
def capture_output():
  # A contextmanger that collects the stdout and stderr of wrapped code

  oldout,olderr = sys.stdout, sys.stderr
  try:
    out = [io.StringIO(), io.StringIO()]
    sys.stdout,sys.stderr = out
    yield out
  finally:
    sys.stdout,sys.stderr = oldout, olderr
    out[0] = out[0].getvalue()
    out[1] = out[1].getvalue()


class ApiStaticCheckUnitTest(unittest.TestCase):
  def setUp(self):
    self.exe_path = os.path.join(REPOSITORY_ROOT, 'out')
    self.temp_dir = tempfile.mkdtemp(dir=self.exe_path)
    os.chdir(self.temp_dir)
    os.mkdir('android')
    with open(API_VERSION_FILENAME, 'w') as api_version_file:
      api_version_file.write('0')
    with open(API_FILENAME, 'w') as api_file:
      api_file.write('}\nStamp: 7d9d25f71cb8a5aba86202540a20d405\n')
    shutil.copytree(os.path.dirname(__file__), 'tools')


  def tearDown(self):
    shutil.rmtree(self.temp_dir)


  def make_jar(self, java, class_name):
    # Compile |java| wrapped in a class named |class_name| to a jar file and
    # return jar filename.

    java_filename = class_name + '.java'
    class_filenames = class_name + '*.class'
    jar_filename = class_name + '.jar'

    with open(java_filename, 'w') as java_file:
      java_file.write('public class %s {' % class_name)
      java_file.write(java)
      java_file.write('}')
    os.system('javac %s' % java_filename)
    os.system('jar cf %s %s' % (jar_filename, class_filenames))
    return jar_filename


  def run_check_api_calls(self, api_java, impl_java):
    test = self
    class MockOpts(object):
      def __init__(self):
        self.api_jar = test.make_jar(api_java, 'Api')
        self.impl_jar = [test.make_jar(impl_java, 'Impl')]
    opts = MockOpts()
    with capture_output() as return_output:
      return_code = api_static_checks.check_api_calls(opts)
    return [return_code, return_output[0]]


  def test_check_api_calls_success(self):
    # Test simple classes with functions
    self.assertEqual(self.run_check_api_calls(
        'void a(){}', 'void b(){}'), [True, ''])
    # Test simple classes with functions calling themselves
    self.assertEqual(self.run_check_api_calls(
        'void a(){} void b(){a();}', 'void c(){} void d(){c();}'), [True, ''])


  def test_check_api_calls_failure(self):
    # Test static call
    self.assertEqual(self.run_check_api_calls(
        'public static void a(){}', 'void b(){Api.a();}'),
        [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n'])
    # Test virtual call
    self.assertEqual(self.run_check_api_calls(
        'public void a(){}', 'void b(){new Api().a();}'),
        [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n'])


  def run_check_api_version(self, java):
    OUT_FILENAME = 'out.txt'
    return_code = os.system('./tools/update_api.py --api_jar %s > %s' %
        (self.make_jar(java, 'Api'), OUT_FILENAME))
    with open(API_FILENAME, 'r') as api_file:
      api = api_file.read()
    with open(API_VERSION_FILENAME, 'r') as api_version_file:
      api_version = api_version_file.read()
    with open(OUT_FILENAME, 'r') as out_file:
      output = out_file.read()

    # Verify stamp
    api_stamp = api.split('\n')[-2]
    stamp_length = len('Stamp: 78418460c193047980ae9eabb79293f2\n')
    api = api[:-stamp_length]
    api_hash = hashlib.md5()
    api_hash.update(api.encode('utf-8'))
    self.assertEqual(api_stamp, 'Stamp: %s' % api_hash.hexdigest())

    return [return_code == 0, output, api, api_version]

  def test_split_by_class_sort(self):
    expected = [
        [
            'public class Api {',
            'public Api();',
            'public void a();',
            'public void b();',
            '}',
        ],
        [
            'public class zee {',
            'public abstract int z();',
            'public void x();',
            'public void y();',
            'public zee();',
            '}',
        ],
    ]
    input = """Compiled from Api.java
public class Api {
public void b();
public Api();
public void a();
}
Compiled from zee.java
public class zee {
public void x();
public zee();
public void y();
public abstract int z();
}
"""
    self.assertEqual(update_api._split_by_class(input.splitlines()), expected)


  def test_update_api_success(self):
    # Test simple new API
    self.assertEqual(self.run_check_api_version(
        'public void a(){}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'])
    # Test version number not increased when API not changed
    self.assertEqual(self.run_check_api_version(
        'public void a(){}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'])
    # Test acceptable API method addition
    self.assertEqual(self.run_check_api_version(
        'public void a(){} public void b(){}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
  public void b();
}
""", '2'])
    # Test version number not increased when API not changed
    self.assertEqual(self.run_check_api_version(
        'public void a(){} public void b(){}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
  public void b();
}
""", '2'])
    # Test acceptable API class addition
    self.assertEqual(self.run_check_api_version(
        'public void a(){} public void b(){} public class C {}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C {
  public Api$C(Api);
}
public class Api {
  public Api();
  public void a();
  public void b();
}
""", '3'])
    # Test version number not increased when API not changed
    self.assertEqual(self.run_check_api_version(
        'public void a(){} public void b(){} public class C {}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C {
  public Api$C(Api);
}
public class Api {
  public Api();
  public void a();
  public void b();
}
""", '3'])


  def test_update_api_failure(self):
    # Create a simple new API
    self.assertEqual(self.run_check_api_version(
        'public void a(){}'),
        [True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'])
    # Test removing API method not allowed
    self.assertEqual(self.run_check_api_version(''),
        [False, ERROR_PREFIX_UPDATE_API + 'public void a();'
            + ERROR_SUFFIX_UPDATE_API,
            CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'])
    # Test modifying API method not allowed
    self.assertEqual(self.run_check_api_version(
        'public void a(int x){}'),
        [False, ERROR_PREFIX_UPDATE_API + 'public void a();'
            + ERROR_SUFFIX_UPDATE_API,
            CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'])