1# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Unit tests for external updater reviewers."""
15
16from typing import List, Mapping, Set
17import unittest
18
19import reviewers
20
21
22class ExternalUpdaterReviewersTest(unittest.TestCase):
23    """Unit tests for external updater reviewers."""
24
25    def setUp(self):
26        super().setUp()
27        # save constants in reviewers
28        self.saved_proj_reviewers = reviewers.PROJ_REVIEWERS
29        self.saved_rust_reviewers = reviewers.RUST_REVIEWERS
30        self.saved_rust_reviewer_list = reviewers.RUST_REVIEWER_LIST
31        self.saved_rust_crate_owners = reviewers.RUST_CRATE_OWNERS
32
33    def tearDown(self):
34        super().tearDown()
35        # restore constants in reviewers
36        reviewers.PROJ_REVIEWERS = self.saved_proj_reviewers
37        reviewers.RUST_REVIEWERS = self.saved_rust_reviewers
38        reviewers.RUST_REVIEWER_LIST = self.saved_rust_reviewer_list
39        reviewers.RUST_CRATE_OWNERS = self.saved_rust_crate_owners
40
41    def _collect_reviewers(self, num_runs, proj_path):
42        counters = {}
43        for _ in range(num_runs):
44            name = reviewers.find_reviewers(proj_path)
45            if name in counters:
46                counters[name] += 1
47            else:
48                counters[name] = 1
49        return counters
50
51    def test_reviewers_types(self):
52        """Check the types of PROJ_REVIEWERS and RUST_REVIEWERS."""
53        # Check type of PROJ_REVIEWERS
54        self.assertIsInstance(reviewers.PROJ_REVIEWERS, Mapping)
55        for key, value in reviewers.PROJ_REVIEWERS.items():
56            self.assertIsInstance(key, str)
57            # pylint: disable=isinstance-second-argument-not-valid-type
58            # https://github.com/PyCQA/pylint/issues/3507
59            if isinstance(value, (List, Set)):
60                for x in value:
61                    self.assertIsInstance(x, str)
62            else:
63                self.assertIsInstance(value, str)
64        # Check element types of the reviewers list and map.
65        self.assertIsInstance(reviewers.RUST_REVIEWERS, Mapping)
66        for (name, quota) in reviewers.RUST_REVIEWERS.items():
67            self.assertIsInstance(name, str)
68            self.assertIsInstance(quota, int)
69
70    def test_reviewers_constants(self):
71        """Check the constants associated to the reviewers."""
72        # There should be enough people in the reviewers pool.
73        self.assertGreaterEqual(len(reviewers.RUST_REVIEWERS), 3)
74        # Assume no project reviewers and recreate RUST_REVIEWER_LIST
75        reviewers.PROJ_REVIEWERS = {}
76        reviewers.RUST_REVIEWER_LIST = reviewers.create_rust_reviewer_list()
77        sum_projects = sum(reviewers.RUST_REVIEWERS.values())
78        self.assertEqual(sum_projects, len(reviewers.RUST_REVIEWER_LIST))
79
80    def test_reviewers_randomness(self):
81        """Check random selection of reviewers."""
82        # This might fail when the random.choice function is extremely unfair.
83        # With N * 20 tries, each reviewer should be picked at least twice.
84        # Assume no project reviewers and recreate RUST_REVIEWER_LIST
85        reviewers.PROJ_REVIEWERS = {}
86        reviewers.RUST_REVIEWER_LIST = reviewers.create_rust_reviewer_list()
87        num_tries = len(reviewers.RUST_REVIEWERS) * 20
88        counters = self._collect_reviewers(num_tries, "rust/crates/libc")
89        self.assertEqual(len(counters), len(reviewers.RUST_REVIEWERS))
90        for n in counters.values():
91            self.assertGreaterEqual(n, 5)
92        self.assertEqual(sum(counters.values()), num_tries)
93
94    def test_project_reviewers(self):
95        """For specific projects, select only the specified reviewers."""
96        reviewers.PROJ_REVIEWERS = {
97            "rust/crates/p1": "x@g.com",
98            "rust/crates/p_any": ["x@g.com", "y@g.com"],
99            "rust/crates/p_all": {"z@g", "x@g.com", "y@g.com"},
100        }
101        counters = self._collect_reviewers(20, "external/rust/crates/p1")
102        self.assertEqual(len(counters), 1)
103        self.assertTrue(counters["r=x@g.com"], 20)
104        counters = self._collect_reviewers(20, "external/rust/crates/p_any")
105        self.assertEqual(len(counters), 2)
106        self.assertGreater(counters["r=x@g.com"], 2)
107        self.assertGreater(counters["r=y@g.com"], 2)
108        self.assertTrue(counters["r=x@g.com"] + counters["r=y@g.com"], 20)
109        counters = self._collect_reviewers(20, "external/rust/crates/p_all")
110        # {x, y, z} reviewers should be sorted
111        self.assertEqual(counters["r=x@g.com,r=y@g.com,r=z@g"], 20)
112
113    def test_weighted_reviewers(self):
114        """Test create_rust_reviewer_list."""
115        reviewers.PROJ_REVIEWERS = {
116            "any_p1": "x@g",  # 1 for x@g
117            "any_p2": {"xyz", "x@g"},  # 1 for x@g, xyz is not a rust reviewer
118            "any_p3": {"abc", "x@g"},  # 0.5 for "abc" and "x@g"
119        }
120        reviewers.RUST_REVIEWERS = {
121            "x@g": 5,  # ceil(5 - 2.5) = 3
122            "abc": 2,  # ceil(2 - 0.5) = 2
123        }
124        reviewer_list = reviewers.create_rust_reviewer_list()
125        self.assertEqual(reviewer_list, ["x@g", "x@g", "x@g", "abc", "abc"])
126        # Error case: if nobody has project quota, reset everyone to 1.
127        reviewers.RUST_REVIEWERS = {
128            "x@g": 1,  # ceil(1 - 2.5) = -1
129            "abc": 0,  # ceil(0 - 0.5) = 0
130        }
131        reviewer_list = reviewers.create_rust_reviewer_list()
132        self.assertEqual(reviewer_list, ["x@g", "abc"])  # everyone got 1
133
134
135if __name__ == "__main__":
136    unittest.main(verbosity=2)
137