diff options
| -rw-r--r-- | tools/apilint/apilint.py | 105 | ||||
| -rwxr-xr-x | tools/apilint/apilint_sha_system.sh | 23 | ||||
| -rw-r--r-- | tools/apilint/apilint_test.py | 147 |
3 files changed, 248 insertions, 27 deletions
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py index b5a990e6e3cf..6476abd8268e 100644 --- a/tools/apilint/apilint.py +++ b/tools/apilint/apilint.py @@ -209,24 +209,42 @@ class Package(): return self.raw -def _parse_stream(f, clazz_cb=None, base_f=None): +def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None, + in_classes_with_base=[]): api = {} + in_classes_with_base = _retry_iterator(in_classes_with_base) if base_f: - base_classes = _parse_stream_to_generator(base_f) + base_classes = _retry_iterator(_parse_stream_to_generator(base_f)) else: base_classes = [] - for clazz in _parse_stream_to_generator(f): - base_class = _parse_to_matching_class(base_classes, clazz) - if base_class: - clazz.merge_from(base_class) - + def handle_class(clazz): if clazz_cb: clazz_cb(clazz) else: # In callback mode, don't keep track of the full API api[clazz.fullname] = clazz + def handle_missed_classes_with_base(clazz): + for c in _yield_until_matching_class(in_classes_with_base, clazz): + base_class = _skip_to_matching_class(base_classes, c) + if base_class: + handle_class(base_class) + + for clazz in _parse_stream_to_generator(f): + # Before looking at clazz, let's see if there's some classes that were not present, but + # may have an entry in the base stream. + handle_missed_classes_with_base(clazz) + + base_class = _skip_to_matching_class(base_classes, clazz) + if base_class: + clazz.merge_from(base_class) + if out_classes_with_base is not None: + out_classes_with_base.append(clazz) + handle_class(clazz) + + handle_missed_classes_with_base(None) + return api def _parse_stream_to_generator(f): @@ -257,18 +275,22 @@ def _parse_stream_to_generator(f): elif raw.startswith(" field"): clazz.fields.append(Field(clazz, line, raw, blame)) elif raw.startswith(" }") and clazz: - while True: - retry = yield clazz - if not retry: - break - # send() was called, asking us to redeliver clazz on next(). Still need to yield - # a dummy value to the send() first though. - if (yield "Returning clazz on next()"): - raise TypeError("send() must be followed by next(), not send()") - - -def _parse_to_matching_class(classes, needle): - """Takes a classes generator and parses it until it returns the class we're looking for + yield clazz + +def _retry_iterator(it): + """Wraps an iterator, such that calling send(True) on it will redeliver the same element""" + for e in it: + while True: + retry = yield e + if not retry: + break + # send() was called, asking us to redeliver clazz on next(). Still need to yield + # a dummy value to the send() first though. + if (yield "Returning clazz on next()"): + raise TypeError("send() must be followed by next(), not send()") + +def _skip_to_matching_class(classes, needle): + """Takes a classes iterator and consumes entries until it returns the class we're looking for This relies on classes being sorted by package and class name.""" @@ -276,8 +298,8 @@ def _parse_to_matching_class(classes, needle): if clazz.pkg.name < needle.pkg.name: # We haven't reached the right package yet continue - if clazz.name < needle.name: - # We haven't reached the right class yet + if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname: + # We're in the right package, but not the right class yet continue if clazz.fullname == needle.fullname: return clazz @@ -285,6 +307,28 @@ def _parse_to_matching_class(classes, needle): classes.send(clazz) return None +def _yield_until_matching_class(classes, needle): + """Takes a class iterator and yields entries it until it reaches the class we're looking for. + + This relies on classes being sorted by package and class name.""" + + for clazz in classes: + if needle is None: + yield clazz + elif clazz.pkg.name < needle.pkg.name: + # We haven't reached the right package yet + yield clazz + elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname: + # We're in the right package, but not the right class yet + yield clazz + elif clazz.fullname == needle.fullname: + # Class found, abort. + return + else: + # We ran past the right class. Send it back into the iterator, then abort. + classes.send(clazz) + return + class Failure(): def __init__(self, sig, clazz, detail, error, rule, msg): self.sig = sig @@ -1543,12 +1587,14 @@ def examine_clazz(clazz): verify_singleton(clazz) -def examine_stream(stream, base_stream=None): +def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None): """Find all style issues in the given API stream.""" global failures, noticed failures = {} noticed = {} - _parse_stream(stream, examine_clazz, base_f=base_stream) + _parse_stream(stream, examine_clazz, base_f=base_stream, + in_classes_with_base=in_classes_with_base, + out_classes_with_base=out_classes_with_base) return (failures, noticed) @@ -1734,19 +1780,24 @@ if __name__ == "__main__": show_stats(cur, prev) sys.exit() + classes_with_base = [] + with current_file as f: if base_current_file: with base_current_file as base_f: - cur_fail, cur_noticed = examine_stream(f, base_f) + cur_fail, cur_noticed = examine_stream(f, base_f, + out_classes_with_base=classes_with_base) else: - cur_fail, cur_noticed = examine_stream(f) + cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base) + if not previous_file is None: with previous_file as f: if base_previous_file: with base_previous_file as base_f: - prev_fail, prev_noticed = examine_stream(f, base_f) + prev_fail, prev_noticed = examine_stream(f, base_f, + in_classes_with_base=classes_with_base) else: - prev_fail, prev_noticed = examine_stream(f) + prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base) # ignore errors from previous API level for p in prev_fail: diff --git a/tools/apilint/apilint_sha_system.sh b/tools/apilint/apilint_sha_system.sh new file mode 100755 index 000000000000..8538a3d904f5 --- /dev/null +++ b/tools/apilint/apilint_sha_system.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if git show --name-only --pretty=format: $1 | grep api/ > /dev/null; then + python tools/apilint/apilint.py \ + --base-current <(git show $1:api/current.txt) \ + --base-previous <(git show $1^:api/current.txt) \ + <(git show $1:api/system-current.txt) \ + <(git show $1^:api/system-current.txt) +fi diff --git a/tools/apilint/apilint_test.py b/tools/apilint/apilint_test.py new file mode 100644 index 000000000000..ece69a99f579 --- /dev/null +++ b/tools/apilint/apilint_test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import apilint + +def cls(pkg, name): + return apilint.Class(apilint.Package(999, "package %s {" % pkg, None), 999, + "public final class %s {" % name, None) + +_ri = apilint._retry_iterator + +c1 = cls("android.app", "ActivityManager") +c2 = cls("android.app", "Notification") +c3 = cls("android.app", "Notification.Action") +c4 = cls("android.graphics", "Bitmap") + +class UtilTests(unittest.TestCase): + def test_retry_iterator(self): + it = apilint._retry_iterator([1, 2, 3, 4]) + self.assertEqual(it.next(), 1) + self.assertEqual(it.next(), 2) + self.assertEqual(it.next(), 3) + it.send("retry") + self.assertEqual(it.next(), 3) + self.assertEqual(it.next(), 4) + with self.assertRaises(StopIteration): + it.next() + + def test_retry_iterator_one(self): + it = apilint._retry_iterator([1]) + self.assertEqual(it.next(), 1) + it.send("retry") + self.assertEqual(it.next(), 1) + with self.assertRaises(StopIteration): + it.next() + + def test_retry_iterator_one(self): + it = apilint._retry_iterator([1]) + self.assertEqual(it.next(), 1) + it.send("retry") + self.assertEqual(it.next(), 1) + with self.assertRaises(StopIteration): + it.next() + + def test_skip_to_matching_class_found(self): + it = _ri([c1, c2, c3, c4]) + self.assertEquals(apilint._skip_to_matching_class(it, c3), + c3) + self.assertEqual(it.next(), c4) + + def test_skip_to_matching_class_not_found(self): + it = _ri([c1, c2, c3, c4]) + self.assertEquals(apilint._skip_to_matching_class(it, cls("android.content", "ContentProvider")), + None) + self.assertEqual(it.next(), c4) + + def test_yield_until_matching_class_found(self): + it = _ri([c1, c2, c3, c4]) + self.assertEquals(list(apilint._yield_until_matching_class(it, c3)), + [c1, c2]) + self.assertEqual(it.next(), c4) + + def test_yield_until_matching_class_not_found(self): + it = _ri([c1, c2, c3, c4]) + self.assertEquals(list(apilint._yield_until_matching_class(it, cls("android.content", "ContentProvider"))), + [c1, c2, c3]) + self.assertEqual(it.next(), c4) + + def test_yield_until_matching_class_None(self): + it = _ri([c1, c2, c3, c4]) + self.assertEquals(list(apilint._yield_until_matching_class(it, None)), + [c1, c2, c3, c4]) + + +faulty_current_txt = """ +package android.app { + public final class Activity { + } + + public final class WallpaperColors implements android.os.Parcelable { + ctor public WallpaperColors(android.os.Parcel); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR; + } +} +""".split('\n') + +ok_current_txt = """ +package android.app { + public final class Activity { + } + + public final class WallpaperColors implements android.os.Parcelable { + ctor public WallpaperColors(); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR; + } +} +""".split('\n') + +system_current_txt = """ +package android.app { + public final class WallpaperColors implements android.os.Parcelable { + method public int getSomething(); + } +} +""".split('\n') + + + +class BaseFileTests(unittest.TestCase): + def test_base_file_avoids_errors(self): + failures, _ = apilint.examine_stream(system_current_txt, ok_current_txt) + self.assertEquals(failures, {}) + + def test_class_with_base_finds_same_errors(self): + failures_with_classes_with_base, _ = apilint.examine_stream("", faulty_current_txt, + in_classes_with_base=[cls("android.app", "WallpaperColors")]) + failures_with_system_txt, _ = apilint.examine_stream(system_current_txt, faulty_current_txt) + + self.assertEquals(failures_with_classes_with_base.keys(), failures_with_system_txt.keys()) + + def test_classes_with_base_is_emited(self): + classes_with_base = [] + _, _ = apilint.examine_stream(system_current_txt, faulty_current_txt, + out_classes_with_base=classes_with_base) + self.assertEquals(map(lambda x: x.fullname, classes_with_base), ["android.app.WallpaperColors"]) + +if __name__ == "__main__": + unittest.main()
\ No newline at end of file |