#!/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'])