1#!/usr/bin/env python3 2# Copyright 2016 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""api_static_checks_unittest.py - Unittests for api_static_checks.py""" 7 8 9import contextlib 10import hashlib 11import io 12import os 13import shutil 14import sys 15import tempfile 16import unittest 17 18REPOSITORY_ROOT = os.path.abspath(os.path.join( 19 os.path.dirname(__file__), '..', '..', '..')) 20 21sys.path.append(os.path.join(REPOSITORY_ROOT, 'components')) 22from cronet.tools import api_static_checks # pylint: disable=wrong-import-position 23 24# pylint: disable=useless-object-inheritance 25 26 27ERROR_PREFIX_CHECK_API_CALLS = ( 28"""ERROR: Found the following calls from implementation classes through 29 API classes. These could fail if older API is used that 30 does not contain newer methods. Please call through a 31 wrapper class from VersionSafeCallbacks. 32""") 33 34 35ERROR_PREFIX_UPDATE_API = ( 36"""ERROR: This API was modified or removed: 37 """) 38 39 40ERROR_SUFFIX_UPDATE_API = ( 41""" 42 43 Cronet API methods and classes cannot be modified. 44""") 45 46 47CHECK_API_VERSION_PREFIX = ( 48"""DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT 49 50""") 51 52 53API_FILENAME = './android/api.txt' 54API_VERSION_FILENAME = './android/api_version.txt' 55 56 57@contextlib.contextmanager 58def capture_output(): 59 # A contextmanger that collects the stdout and stderr of wrapped code 60 61 oldout,olderr = sys.stdout, sys.stderr 62 try: 63 out = [io.StringIO(), io.StringIO()] 64 sys.stdout,sys.stderr = out 65 yield out 66 finally: 67 sys.stdout,sys.stderr = oldout, olderr 68 out[0] = out[0].getvalue() 69 out[1] = out[1].getvalue() 70 71 72class ApiStaticCheckUnitTest(unittest.TestCase): 73 def setUp(self): 74 self.exe_path = os.path.join(REPOSITORY_ROOT, 'out') 75 self.temp_dir = tempfile.mkdtemp(dir=self.exe_path) 76 os.chdir(self.temp_dir) 77 os.mkdir('android') 78 with open(API_VERSION_FILENAME, 'w') as api_version_file: 79 api_version_file.write('0') 80 with open(API_FILENAME, 'w') as api_file: 81 api_file.write('}\nStamp: 7d9d25f71cb8a5aba86202540a20d405\n') 82 shutil.copytree(os.path.dirname(__file__), 'tools') 83 84 85 def tearDown(self): 86 shutil.rmtree(self.temp_dir) 87 88 89 def make_jar(self, java, class_name): 90 # Compile |java| wrapped in a class named |class_name| to a jar file and 91 # return jar filename. 92 93 java_filename = class_name + '.java' 94 class_filenames = class_name + '*.class' 95 jar_filename = class_name + '.jar' 96 97 with open(java_filename, 'w') as java_file: 98 java_file.write('public class %s {' % class_name) 99 java_file.write(java) 100 java_file.write('}') 101 os.system('javac %s' % java_filename) 102 os.system('jar cf %s %s' % (jar_filename, class_filenames)) 103 return jar_filename 104 105 106 def run_check_api_calls(self, api_java, impl_java): 107 test = self 108 class MockOpts(object): 109 def __init__(self): 110 self.api_jar = test.make_jar(api_java, 'Api') 111 self.impl_jar = [test.make_jar(impl_java, 'Impl')] 112 opts = MockOpts() 113 with capture_output() as return_output: 114 return_code = api_static_checks.check_api_calls(opts) 115 return [return_code, return_output[0]] 116 117 118 def test_check_api_calls_success(self): 119 # Test simple classes with functions 120 self.assertEqual(self.run_check_api_calls( 121 'void a(){}', 'void b(){}'), [True, '']) 122 # Test simple classes with functions calling themselves 123 self.assertEqual(self.run_check_api_calls( 124 'void a(){} void b(){a();}', 'void c(){} void d(){c();}'), [True, '']) 125 126 127 def test_check_api_calls_failure(self): 128 # Test static call 129 self.assertEqual(self.run_check_api_calls( 130 'public static void a(){}', 'void b(){Api.a();}'), 131 [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n']) 132 # Test virtual call 133 self.assertEqual(self.run_check_api_calls( 134 'public void a(){}', 'void b(){new Api().a();}'), 135 [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n']) 136 137 138 def run_check_api_version(self, java): 139 OUT_FILENAME = 'out.txt' 140 return_code = os.system('./tools/update_api.py --api_jar %s > %s' % 141 (self.make_jar(java, 'Api'), OUT_FILENAME)) 142 with open(API_FILENAME, 'r') as api_file: 143 api = api_file.read() 144 with open(API_VERSION_FILENAME, 'r') as api_version_file: 145 api_version = api_version_file.read() 146 with open(OUT_FILENAME, 'r') as out_file: 147 output = out_file.read() 148 149 # Verify stamp 150 api_stamp = api.split('\n')[-2] 151 stamp_length = len('Stamp: 78418460c193047980ae9eabb79293f2\n') 152 api = api[:-stamp_length] 153 api_hash = hashlib.md5() 154 api_hash.update(api.encode('utf-8')) 155 self.assertEqual(api_stamp, 'Stamp: %s' % api_hash.hexdigest()) 156 157 return [return_code == 0, output, api, api_version] 158 159 160 def test_update_api_success(self): 161 # Test simple new API 162 self.assertEqual(self.run_check_api_version( 163 'public void a(){}'), 164 [True, '', CHECK_API_VERSION_PREFIX + """public class Api { 165 public Api(); 166 public void a(); 167} 168""", '1']) 169 # Test version number not increased when API not changed 170 self.assertEqual(self.run_check_api_version( 171 'public void a(){}'), 172 [True, '', CHECK_API_VERSION_PREFIX + """public class Api { 173 public Api(); 174 public void a(); 175} 176""", '1']) 177 # Test acceptable API method addition 178 self.assertEqual(self.run_check_api_version( 179 'public void a(){} public void b(){}'), 180 [True, '', CHECK_API_VERSION_PREFIX + """public class Api { 181 public Api(); 182 public void a(); 183 public void b(); 184} 185""", '2']) 186 # Test version number not increased when API not changed 187 self.assertEqual(self.run_check_api_version( 188 'public void a(){} public void b(){}'), 189 [True, '', CHECK_API_VERSION_PREFIX + """public class Api { 190 public Api(); 191 public void a(); 192 public void b(); 193} 194""", '2']) 195 # Test acceptable API class addition 196 self.assertEqual(self.run_check_api_version( 197 'public void a(){} public void b(){} public class C {}'), 198 [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C { 199 public Api$C(Api); 200} 201public class Api { 202 public Api(); 203 public void a(); 204 public void b(); 205} 206""", '3']) 207 # Test version number not increased when API not changed 208 self.assertEqual(self.run_check_api_version( 209 'public void a(){} public void b(){} public class C {}'), 210 [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C { 211 public Api$C(Api); 212} 213public class Api { 214 public Api(); 215 public void a(); 216 public void b(); 217} 218""", '3']) 219 220 221 def test_update_api_failure(self): 222 # Create a simple new API 223 self.assertEqual(self.run_check_api_version( 224 'public void a(){}'), 225 [True, '', CHECK_API_VERSION_PREFIX + """public class Api { 226 public Api(); 227 public void a(); 228} 229""", '1']) 230 # Test removing API method not allowed 231 self.assertEqual(self.run_check_api_version(''), 232 [False, ERROR_PREFIX_UPDATE_API + 'public void a();' 233 + ERROR_SUFFIX_UPDATE_API, 234 CHECK_API_VERSION_PREFIX + """public class Api { 235 public Api(); 236 public void a(); 237} 238""", '1']) 239 # Test modifying API method not allowed 240 self.assertEqual(self.run_check_api_version( 241 'public void a(int x){}'), 242 [False, ERROR_PREFIX_UPDATE_API + 'public void a();' 243 + ERROR_SUFFIX_UPDATE_API, 244 CHECK_API_VERSION_PREFIX + """public class Api { 245 public Api(); 246 public void a(); 247} 248""", '1']) 249