summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/apilint/apilint.py105
-rwxr-xr-xtools/apilint/apilint_sha_system.sh23
-rw-r--r--tools/apilint/apilint_test.py147
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