• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2022 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""Tests for keep_sorted."""
16
17from pathlib import Path
18import tempfile
19import textwrap
20from typing import Sequence
21import unittest
22from unittest.mock import MagicMock
23
24from pw_presubmit import keep_sorted
25
26# Only include these literals here so keep_sorted doesn't try to reorder later
27# test lines.
28START = keep_sorted.START
29END = keep_sorted.END
30
31# pylint: disable=attribute-defined-outside-init
32# pylint: disable=too-many-public-methods
33
34
35class TestKeepSorted(unittest.TestCase):
36    """Test KeepSorted class"""
37
38    def _run(self, contents: str) -> None:
39        self.ctx = MagicMock()
40        self.ctx.fail = MagicMock()
41
42        with tempfile.TemporaryDirectory() as tempdir:
43            path = Path(tempdir) / 'foo'
44
45            with path.open('w') as outs:
46                outs.write(contents)
47
48            self.errors: dict[Path, Sequence[str]] = {}
49
50            # pylint: disable=protected-access
51            self.sorter = keep_sorted._FileSorter(self.ctx, path, self.errors)
52
53            # pylint: enable=protected-access
54
55            self.sorter.sort()
56
57            # Truncate the file so it's obvious whether write() changed
58            # anything.
59            with path.open('w') as outs:
60                outs.write('')
61
62            self.sorter.write(path)
63            with path.open() as ins:
64                self.contents = ins.read()
65
66    def assert_errors(self):
67        self.assertTrue(self.errors)
68
69    def assert_no_errors(self):
70        self.assertFalse(self.errors)
71
72    def test_missing_end(self) -> None:
73        with self.assertRaises(keep_sorted.KeepSortedParsingError):
74            self._run(f'{START}\n')
75
76    def test_missing_start(self) -> None:
77        with self.assertRaises(keep_sorted.KeepSortedParsingError):
78            self._run(f'{END}: end\n')
79
80    def test_repeated_start(self) -> None:
81        with self.assertRaises(keep_sorted.KeepSortedParsingError):
82            self._run(f'{START}\n{START}\n')
83
84    def test_unrecognized_directive(self) -> None:
85        with self.assertRaises(keep_sorted.KeepSortedParsingError):
86            self._run(f'{START} foo bar baz\n2\n1\n{END}\n')
87
88    def test_repeated_valid_directive(self) -> None:
89        with self.assertRaises(keep_sorted.KeepSortedParsingError):
90            self._run(f'{START} ignore-case ignore-case\n2\n1\n{END}\n')
91
92    def test_already_sorted(self) -> None:
93        self._run(f'{START}\n1\n2\n3\n4\n{END}\n')
94        self.assert_no_errors()
95        self.assertEqual(self.contents, '')
96
97    def test_not_sorted(self) -> None:
98        self._run(f'{START}\n4\n3\n2\n1\n{END}\n')
99        self.assert_errors()
100        self.assertEqual(self.contents, f'{START}\n1\n2\n3\n4\n{END}\n')
101
102    def test_prefix_sorted(self) -> None:
103        self._run(f'foo\nbar\n{START}\n1\n2\n{END}\n')
104        self.assert_no_errors()
105        self.assertEqual(self.contents, '')
106
107    def test_prefix_not_sorted(self) -> None:
108        self._run(f'foo\nbar\n{START}\n2\n1\n{END}\n')
109        self.assert_errors()
110        self.assertEqual(self.contents, f'foo\nbar\n{START}\n1\n2\n{END}\n')
111
112    def test_suffix_sorted(self) -> None:
113        self._run(f'{START}\n1\n2\n{END}\nfoo\nbar\n')
114        self.assert_no_errors()
115        self.assertEqual(self.contents, '')
116
117    def test_suffix_not_sorted(self) -> None:
118        self._run(f'{START}\n2\n1\n{END}\nfoo\nbar\n')
119        self.assert_errors()
120        self.assertEqual(self.contents, f'{START}\n1\n2\n{END}\nfoo\nbar\n')
121
122    def test_not_sorted_case_sensitive(self) -> None:
123        self._run(f'{START}\na\nD\nB\nc\n{END}\n')
124        self.assert_errors()
125        self.assertEqual(self.contents, f'{START}\nB\nD\na\nc\n{END}\n')
126
127    def test_not_sorted_case_insensitive(self) -> None:
128        self._run(f'{START} ignore-case\na\nD\nB\nc\n{END}\n')
129        self.assert_errors()
130        self.assertEqual(
131            self.contents, f'{START} ignore-case\na\nB\nc\nD\n{END}\n'
132        )
133
134    def test_remove_dupes(self) -> None:
135        self._run(f'{START}\n1\n2\n2\n1\n{END}\n')
136        self.assert_errors()
137        self.assertEqual(self.contents, f'{START}\n1\n2\n{END}\n')
138
139    def test_allow_dupes(self) -> None:
140        self._run(f'{START} allow-dupes\n1\n2\n2\n1\n{END}\n')
141        self.assert_errors()
142        self.assertEqual(
143            self.contents, f'{START} allow-dupes\n1\n1\n2\n2\n{END}\n'
144        )
145
146    def test_case_insensitive_dupes(self) -> None:
147        self._run(f'{START} ignore-case\na\nB\nA\n{END}\n')
148        self.assert_errors()
149        self.assertEqual(
150            self.contents, f'{START} ignore-case\nA\na\nB\n{END}\n'
151        )
152
153    def test_ignored_prefixes(self) -> None:
154        self._run(f'{START} ignore-prefix=foo,bar\na\nb\nfoob\nbarc\n{END}\n')
155        self.assert_no_errors()
156
157    def test_ignored_longest_prefixes(self) -> None:
158        self._run(f'{START} ignore-prefix=1,123\na\n123b\nb\n1c\n{END}\n')
159        self.assert_no_errors()
160
161    def test_ignored_prefixes_whitespace(self) -> None:
162        self._run(
163            f'{START} ignore-prefix=foo,bar\n' f' a\n b\n foob\n barc\n{END}\n'
164        )
165        self.assert_no_errors()
166
167    def test_ignored_prefixes_insensitive(self) -> None:
168        self._run(
169            f'{START} ignore-prefix=foo,bar ignore-case\n'
170            f'a\nB\nfooB\nbarc\n{END}\n'
171        )
172        self.assert_no_errors()
173
174    def test_python_comment_marks_sorted(self) -> None:
175        self._run(f'# {START}\n1\n2\n# {END}\n')
176        self.assert_no_errors()
177
178    def test_python_comment_marks_not_sorted(self) -> None:
179        self._run(f'# {START}\n2\n1\n# {END}\n')
180        self.assert_errors()
181        self.assertEqual(self.contents, f'# {START}\n1\n2\n# {END}\n')
182
183    def test_python_comment_sticky_sorted(self) -> None:
184        self._run(f'# {START}\n# A\n1\n2\n# {END}\n')
185        self.assert_no_errors()
186
187    def test_python_comment_sticky_not_sorted(self) -> None:
188        self._run(f'# {START}\n2\n# A\n1\n# {END}\n')
189        self.assert_errors()
190        self.assertEqual(self.contents, f'# {START}\n# A\n1\n2\n# {END}\n')
191
192    def test_python_comment_sticky_disabled(self) -> None:
193        self._run(f'# {START} sticky-comments=no\n1\n# B\n2\n# {END}\n')
194        self.assert_errors()
195        self.assertEqual(
196            self.contents, f'# {START} sticky-comments=no\n# B\n1\n2\n# {END}\n'
197        )
198
199    def test_cpp_comment_marks_sorted(self) -> None:
200        self._run(f'// {START}\n1\n2\n// {END}\n')
201        self.assert_no_errors()
202
203    def test_cpp_comment_marks_not_sorted(self) -> None:
204        self._run(f'// {START}\n2\n1\n// {END}\n')
205        self.assert_errors()
206        self.assertEqual(self.contents, f'// {START}\n1\n2\n// {END}\n')
207
208    def test_cpp_comment_sticky_sorted(self) -> None:
209        self._run(f'// {START}\n1\n// B\n2\n// {END}\n')
210        self.assert_no_errors()
211
212    def test_cpp_comment_sticky_not_sorted(self) -> None:
213        self._run(f'// {START}\n// B\n2\n1\n// {END}\n')
214        self.assert_errors()
215        self.assertEqual(self.contents, f'// {START}\n1\n// B\n2\n// {END}\n')
216
217    def test_cpp_comment_sticky_disabled(self) -> None:
218        self._run(f'// {START} sticky-comments=no\n1\n// B\n2\n// {END}\n')
219        self.assert_errors()
220        self.assertEqual(
221            self.contents,
222            f'// {START} sticky-comments=no\n// B\n1\n2\n// {END}\n',
223        )
224
225    def test_custom_comment_sticky_sorted(self) -> None:
226        self._run(f'{START} sticky-comments=%\n1\n% B\n2\n{END}\n')
227        self.assert_no_errors()
228
229    def test_custom_comment_sticky_not_sorted(self) -> None:
230        self._run(f'{START} sticky-comments=%\n% B\n2\n1\n{END}\n')
231        self.assert_errors()
232        self.assertEqual(
233            self.contents, f'{START} sticky-comments=%\n1\n% B\n2\n{END}\n'
234        )
235
236    def test_multiline_comment_sticky_sorted(self) -> None:
237        self._run(f'# {START}\n# B\n# A\n1\n2\n# {END}\n')
238        self.assert_no_errors()
239
240    def test_multiline_comment_sticky_not_sorted(self) -> None:
241        self._run(f'# {START}\n# B\n# A\n2\n1\n# {END}\n')
242        self.assert_errors()
243        self.assertEqual(self.contents, f'# {START}\n1\n# B\n# A\n2\n# {END}\n')
244
245    def test_comment_sticky_sorted_fallback_sorted(self) -> None:
246        self._run(f'# {START}\n# A\n1\n# B\n1\n# {END}\n')
247        self.assert_no_errors()
248
249    def test_comment_sticky_sorted_fallback_not_sorted(self) -> None:
250        self._run(f'# {START}\n# B\n1\n# A\n1\n# {END}\n')
251        self.assert_errors()
252        self.assertEqual(self.contents, f'# {START}\n# A\n1\n# B\n1\n# {END}\n')
253
254    def test_comment_sticky_sorted_fallback_dupes(self) -> None:
255        self._run(f'# {START} allow-dupes\n# A\n1\n# A\n1\n# {END}\n')
256        self.assert_no_errors()
257
258    def test_different_comment_sticky_not_sorted(self) -> None:
259        self._run(f'# {START} sticky-comments=%\n% A\n1\n# B\n2\n# {END}\n')
260        self.assert_errors()
261        self.assertEqual(
262            self.contents,
263            f'# {START} sticky-comments=%\n# B\n% A\n1\n2\n# {END}\n',
264        )
265
266    def test_continuation_sorted(self) -> None:
267        initial = textwrap.dedent(
268            f"""
269            # {START}
270            baz
271             abc
272            foo
273              bar
274            # {END}
275            """.lstrip(
276                '\n'
277            )
278        )
279
280        self._run(initial)
281        self.assert_no_errors()
282
283    def test_continuation_not_sorted(self) -> None:
284        initial = textwrap.dedent(
285            f"""
286            # {START}
287            foo
288              bar
289            baz
290             abc
291            # {END}
292            """.lstrip(
293                '\n'
294            )
295        )
296
297        expected = textwrap.dedent(
298            f"""
299            # {START}
300            baz
301             abc
302            foo
303              bar
304            # {END}
305            """.lstrip(
306                '\n'
307            )
308        )
309
310        self._run(initial)
311        self.assert_errors()
312        self.assertEqual(self.contents, expected)
313
314    def test_indented_continuation_sorted(self) -> None:
315        # Intentionally not using textwrap.dedent().
316        initial = f"""
317        # {START}
318        baz
319         abc
320        foo
321          bar
322        # {END}""".lstrip(
323            '\n'
324        )
325
326        self._run(initial)
327        self.assert_no_errors()
328
329    def test_indented_continuation_not_sorted(self) -> None:
330        # Intentionally not using textwrap.dedent().
331        initial = f"""
332        # {START}
333        foo
334          bar
335        baz
336         abc
337        # {END}""".lstrip(
338            '\n'
339        )
340
341        expected = f"""
342        # {START}
343        baz
344         abc
345        foo
346          bar
347        # {END}""".lstrip(
348            '\n'
349        )
350
351        self._run(initial)
352        self.assert_errors()
353        self.assertEqual(self.contents, expected)
354
355
356if __name__ == '__main__':
357    unittest.main()
358