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