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