1#!/usr/bin/env python3
2#
3# Copyright (C) 2020 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from build_log_simplifier import collapse_consecutive_blank_lines
18from build_log_simplifier import collapse_tasks_having_no_output
19from build_log_simplifier import extract_task_names
20from build_log_simplifier import remove_unmatched_exemptions
21from build_log_simplifier import suggest_missing_exemptions
22from build_log_simplifier import normalize_paths
23from build_log_simplifier import regexes_matcher
24from build_log_simplifier import remove_control_characters
25import re
26
27def fail(message):
28    print(message)
29    exit(1)
30
31def test_regexes_matcher_get_matching_regexes():
32    print("test_regexes_matcher_get_matching_regexes")
33    # For each of the given queries, we ask a regexes_matcher to identify which regexes
34    # match them, and compare to the right answer
35    queries = ["", "a", "aa", "aaa", "b", "bb", "ab", "c", "a*"]
36    regexes = ["a", "a*", "aa*", "b", "b*", "a*b"]
37    matcher = regexes_matcher(regexes)
38    for query in queries:
39        simple_matches = [regex for regex in regexes if re.compile(regex).fullmatch(query)]
40        fast_matches = matcher.get_matching_regexes(query)
41        if simple_matches != fast_matches:
42            fail("regexes_matcher returned incorrect results for '" + query + "'. Expected = " + str(simple_matches) + ", actual = " + str(fast_matches))
43        print("Query = '" + query + "', matching regexes = " + str(simple_matches))
44
45def test_normalize_paths():
46    print("test_normalize_paths")
47    lines = [
48      "CHECKOUT=/usr/home/me/workspace",
49      "/usr/home/me/workspace/external/protobuf/somefile.h: somewarning",
50      "Building CXX object protobuf-target/CMakeFiles/libprotobuf.dir/usr/home/me/workspace/external/somefile.cc"
51    ]
52    expected_normalized = [
53      "CHECKOUT=$CHECKOUT",
54      "$CHECKOUT/external/protobuf/somefile.h: somewarning",
55      "Building CXX object protobuf-target/CMakeFiles/libprotobuf.dir$CHECKOUT/external/somefile.cc"
56    ]
57    actual = normalize_paths(lines)
58    if expected_normalized != actual:
59        fail("test_normalize_paths returned incorrect response.\n" +
60            "Input: " + str(lines) + "\n" +
61            "Output: " + str(actual) + "\n" +
62            "Expected output: " + str(expected_normalized)
63        )
64
65def test_regexes_matcher_index_first_matching_regex():
66    print("test_regexes_matcher_index_first_matching_regex")
67    regexes = ["first", "double", "single", "double"]
68    matcher = regexes_matcher(regexes)
69    assert(matcher.index_first_matching_regex("first") == 0)
70    assert(matcher.index_first_matching_regex("double") == 1)
71    assert(matcher.index_first_matching_regex("single") == 2)
72    assert(matcher.index_first_matching_regex("absent") is None)
73
74def test_detect_task_names():
75    print("test_detect_task_names")
76    lines = [
77        "> Task :one\n",
78        "some output\n",
79        "> Task :two\n",
80        "more output\n"
81    ]
82    task_names = [":one", ":two"]
83    detected_names = extract_task_names(lines)
84    if detected_names != task_names:
85        fail("extract_task_names returned incorrect response\n" +
86            "Input   : " + str(lines) + "\n" +
87            "Output  : " + str(detected_names) + "\n" +
88            "Expected: " + str(task_names)
89        )
90
91def test_remove_unmatched_exemptions():
92    print("test_remove_unmatched_exemptions")
93    lines = [
94        "task two message one",
95        "task four message one",
96    ]
97
98    current_config = [
99        "# > Task :one",
100        "task one message one",
101        "# TODO(bug): remove this",
102        "# > Task :two",
103        "task two message one",
104        "# TODO(bug): remove this too",
105        "# > Task :three",
106        "task three message one",
107        "# > Task :four",
108        "task four message one",
109        "# TODO: maybe remove this too?",
110        "# > Task :five",
111        "task five message one"
112    ]
113
114    expected_config = [
115        "# TODO(bug): remove this",
116        "# > Task :two",
117        "task two message one",
118        "# > Task :four",
119        "task four message one",
120    ]
121
122    actual_updated_config = remove_unmatched_exemptions(lines, current_config)
123    if actual_updated_config != expected_config:
124        fail("test_remove_unmatched_exemptions gave incorrect response.\n\n" +
125            "Input log             : " + str(lines) + "\n\n" +
126            "Input config          : " + str(current_config) + "\n\n" +
127            "Expected output config: " + str(expected_config) + "\n\n" +
128            "Actual output config  : " + str(actual_updated_config))
129
130def test_suggest_missing_exemptions():
131    print("test_suggest_missing_exemptions")
132    lines = [
133        "> Task :one",
134        "task one message one",
135        "task one message two",
136        "> Task :two",
137        "task two message one",
138        "duplicate line",
139        "> Task :three",
140        "task three message one",
141        "duplicate line"
142    ]
143
144    expect_config = [
145        "# > Task :one",
146        "task one message one",
147        "task one message two",
148        "# > Task :two",
149        "task two message one",
150        "duplicate line",
151        "# > Task :three",
152        "task three message one"
153    ]
154
155    # generate config starting with nothing
156    validate_suggested_exemptions(lines, [], expect_config)
157
158    # remove one line from config, regenerate config, line should return
159    config2 = expect_config[:1] + expect_config[2:]
160    validate_suggested_exemptions(lines, config2, expect_config)
161
162    # if there is an existing config with the tasks in the other order, the tasks should stay in that order
163    # and the new line should be inserted after the previous matching line
164    config3 = [
165        "# > Task :two",
166        "task two message one",
167	"duplicate line",
168        "# > Task :one",
169        "task one message two",
170        "# > Task :three",
171        "task three message one"
172    ]
173    expect_config3 = [
174        "# > Task :two",
175        "task two message one",
176	"duplicate line",
177        "# > Task :one",
178        "task one message one",
179        "task one message two",
180        "# > Task :three",
181        "task three message one"
182    ]
183    validate_suggested_exemptions(lines, config3, expect_config3)
184
185    # also validate that "> Configure project" gets ignored too
186    config4 = [
187        "# > Configure project a",
188        "some warning"
189    ]
190    lines4 = [
191        "> Configure project b",
192        "some warning"
193    ]
194    expect_config4 = config4
195    validate_suggested_exemptions(lines4, config4, expect_config4)
196
197def test_collapse_tasks_having_no_output():
198    print("test_collapse_tasks_having_no_output")
199    lines = [
200        "> Task :no-output1",
201        "> Task :some-output1",
202        "output1",
203        "> Task :empty-output",
204        "",
205        "> Task :blanks-around-output",
206        "",
207        "output inside blanks",
208        "",
209        "> Task :no-output2",
210        "> Task :no-output3",
211        "FAILURE: Build failed with an exception.\n"
212    ]
213    expected = [
214        "> Task :some-output1",
215        "output1",
216	"> Task :blanks-around-output",
217        "",
218        "output inside blanks",
219        ""
220    ]
221    actual = collapse_tasks_having_no_output(lines)
222    if (actual != expected):
223        fail("collapse_tasks_having_no_output gave incorrect error.\n" +
224            "Expected: " + str(expected) + "\n" +
225            "Actual = " + str(actual))
226
227def test_collapse_consecutive_blank_lines():
228    print("test_collapse_consecutive_blank_lines")
229    lines = [
230        "",
231        "> Task :a",
232        "",
233        "   ",
234        "\n\n",
235        "> Task :b",
236        " ",
237        ""
238    ]
239    expected_collapsed = [
240        "> Task :a",
241        "",
242        "> Task :b",
243        " "
244    ]
245    actual_collapsed = collapse_consecutive_blank_lines(lines)
246    if actual_collapsed != expected_collapsed:
247        fail("collapse_consecutive_blank_lines returned incorrect response.\n"
248            "Input: " + lines + "\n" +
249            "Output: " + actual_collapsed + "\n" +
250            "Expected output: " + expected_collapsed
251        )
252
253def validate_suggested_exemptions(lines, config, expected_config):
254    suggested_config = suggest_missing_exemptions(lines, config)
255    if suggested_config != expected_config:
256        fail("suggest_missing_exemptions incorrect response.\n" +
257             "Lines: " + str(lines) + ",\n" +
258             "config: " + str(config) + ",\n" +
259             "expected suggestion: " + str(expected_config) + ",\n"
260             "actual suggestion  : " + str(suggested_config))
261
262def test_remove_control_characters():
263    print("test_remove_control_characters")
264    given = [
265        # a line starting with several color codes in it
266        "src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java:39: warning: Method androidx.arch.core.internal.FastSafeIterableMap.get(K) references hidden type androidx.arch.core.internal.SafeIterableMap.Entry<K,V>. [HiddenTypeParameter]",
267        # a line with a variety of characters, none of which are color codes
268        "space tab\tCAPITAL underscore_ slash/ colon: number 1 newline\n",
269    ]
270    expected = [
271        "src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java:39: warning: Method androidx.arch.core.internal.FastSafeIterableMap.get(K) references hidden type androidx.arch.core.internal.SafeIterableMap.Entry<K,V>. [HiddenTypeParameter]",
272        "space tab\tCAPITAL underscore_ slash/ colon: number 1 newline\n",
273    ]
274    actual = [remove_control_characters(line) for line in given]
275    if actual != expected:
276        fail("remove_control_charactres gave incorrect response.\n\n" +
277            "Input          : " + str(given) + ".\n\n" +
278            "Expected output: " + str(expected) + ".\n\n" +
279            "Actual output  : " + str(actual) + ".")
280
281
282def main():
283    test_collapse_consecutive_blank_lines()
284    test_collapse_tasks_having_no_output()
285    test_detect_task_names()
286    test_suggest_missing_exemptions()
287    test_normalize_paths()
288    test_regexes_matcher_get_matching_regexes()
289    test_regexes_matcher_index_first_matching_regex()
290    test_remove_control_characters()
291    test_remove_unmatched_exemptions()
292
293if __name__ == "__main__":
294    main()
295