|
7 | 7 | LicenseInfo, |
8 | 8 | _is_copyright_line, |
9 | 9 | _license_line_matches, |
| 10 | + _normalize_copyright_line, |
| 11 | + any_copyright_line_found, |
10 | 12 | copyright_sentinel_found, |
11 | 13 | find_license_header_index, |
12 | 14 | main, |
@@ -80,6 +82,76 @@ def test_reuse_spdx_license_identifier_is_not_copyright(self): |
80 | 82 | assert not _is_copyright_line('# SPDX-License-Identifier: Apache-2.0') |
81 | 83 |
|
82 | 84 |
|
| 85 | +# --------------------------------------------------------------------------- |
| 86 | +# _normalize_copyright_line |
| 87 | +# --------------------------------------------------------------------------- |
| 88 | + |
| 89 | + |
| 90 | +class TestNormalizeCopyrightLine: |
| 91 | + def test_strips_c_symbol(self): |
| 92 | + assert _normalize_copyright_line('# Copyright (c) 2026 Acme Corp') == '# Copyright 2026 Acme Corp' |
| 93 | + |
| 94 | + def test_strips_c_symbol_uppercase(self): |
| 95 | + assert _normalize_copyright_line('# Copyright (C) 2026 Acme Corp') == '# Copyright 2026 Acme Corp' |
| 96 | + |
| 97 | + def test_strips_present_suffix(self): |
| 98 | + assert _normalize_copyright_line('# Copyright 2025-present Acme Corp') == '# Copyright 2025 Acme Corp' |
| 99 | + |
| 100 | + def test_strips_all_rights_reserved(self): |
| 101 | + assert ( |
| 102 | + _normalize_copyright_line('# Copyright 2026 Acme Corp. All rights reserved') == '# Copyright 2026 Acme Corp' |
| 103 | + ) |
| 104 | + |
| 105 | + def test_strips_all_combined(self): |
| 106 | + result = _normalize_copyright_line('// Copyright (c) 2025-present Acme Corp. All rights reserved') |
| 107 | + assert result == '// Copyright 2025 Acme Corp' |
| 108 | + |
| 109 | + def test_leaves_plain_line_unchanged(self): |
| 110 | + assert _normalize_copyright_line('# Copyright 2026 Acme Corp') == '# Copyright 2026 Acme Corp' |
| 111 | + |
| 112 | + |
| 113 | +# --------------------------------------------------------------------------- |
| 114 | +# _license_line_matches — copyright decoration tolerance (no wildcard needed) |
| 115 | +# --------------------------------------------------------------------------- |
| 116 | + |
| 117 | + |
| 118 | +class TestLicenseLineMatchesDecoration: |
| 119 | + def test_c_symbol_matches_plain_template(self): |
| 120 | + assert _license_line_matches( |
| 121 | + '// Copyright 2026 Acme Corp', |
| 122 | + '// Copyright (c) 2026 Acme Corp', |
| 123 | + match_years_strictly=False, |
| 124 | + ) |
| 125 | + |
| 126 | + def test_present_suffix_matches_plain_template(self): |
| 127 | + assert _license_line_matches( |
| 128 | + '// Copyright 2026 Acme Corp', |
| 129 | + '// Copyright 2025-present Acme Corp', |
| 130 | + match_years_strictly=False, |
| 131 | + ) |
| 132 | + |
| 133 | + def test_all_rights_reserved_matches_plain_template(self): |
| 134 | + assert _license_line_matches( |
| 135 | + '// Copyright 2026 Acme Corp', |
| 136 | + '// Copyright 2026 Acme Corp. All rights reserved', |
| 137 | + match_years_strictly=False, |
| 138 | + ) |
| 139 | + |
| 140 | + def test_all_decorations_combined(self): |
| 141 | + assert _license_line_matches( |
| 142 | + '// Copyright 2026 Acme Corp', |
| 143 | + '// Copyright (c) 2025-present Acme Corp. All rights reserved', |
| 144 | + match_years_strictly=False, |
| 145 | + ) |
| 146 | + |
| 147 | + def test_wrong_org_still_fails(self): |
| 148 | + assert not _license_line_matches( |
| 149 | + '// Copyright 2026 Acme Corp', |
| 150 | + '// Copyright (c) 2025-present Other Corp. All rights reserved', |
| 151 | + match_years_strictly=False, |
| 152 | + ) |
| 153 | + |
| 154 | + |
83 | 155 | # --------------------------------------------------------------------------- |
84 | 156 | # _license_line_matches — wildcard_copyright_org |
85 | 157 | # --------------------------------------------------------------------------- |
@@ -138,6 +210,33 @@ def test_sentinel_beyond_top_lines_not_detected(self): |
138 | 210 | assert not copyright_sentinel_found(content, top_lines_count=2) |
139 | 211 |
|
140 | 212 |
|
| 213 | +# --------------------------------------------------------------------------- |
| 214 | +# any_copyright_line_found |
| 215 | +# --------------------------------------------------------------------------- |
| 216 | + |
| 217 | + |
| 218 | +class TestAnyCopyrightLineFound: |
| 219 | + def test_finds_standard_copyright(self): |
| 220 | + content = ['# Copyright (c) 2025-present Acme Corp. All rights reserved\n', 'import foo\n'] |
| 221 | + assert any_copyright_line_found(content, top_lines_count=5) |
| 222 | + |
| 223 | + def test_finds_cpp_style(self): |
| 224 | + content = ['// Copyright (c) 2025-present Acme Corp. All rights reserved\n', 'int x;\n'] |
| 225 | + assert any_copyright_line_found(content, top_lines_count=5) |
| 226 | + |
| 227 | + def test_not_found_in_plain_code(self): |
| 228 | + content = ['import foo\n', 'x = 1\n'] |
| 229 | + assert not any_copyright_line_found(content, top_lines_count=5) |
| 230 | + |
| 231 | + def test_sentinel_line_also_counts(self): |
| 232 | + content = [f'# Copyright 2026 {COPYRIGHT_ORG_SENTINEL}\n'] |
| 233 | + assert any_copyright_line_found(content, top_lines_count=5) |
| 234 | + |
| 235 | + def test_beyond_top_lines_not_found(self): |
| 236 | + content = ['import foo\n', 'import bar\n', '# Copyright 2026 Acme\n'] |
| 237 | + assert not any_copyright_line_found(content, top_lines_count=2) |
| 238 | + |
| 239 | + |
141 | 240 | # --------------------------------------------------------------------------- |
142 | 241 | # find_license_header_index — wildcard matching |
143 | 242 | # --------------------------------------------------------------------------- |
@@ -280,3 +379,52 @@ def test_without_wildcard_rejects_different_org(self, tmp_path): |
280 | 379 | ]) |
281 | 380 | assert ret == 1 |
282 | 381 | assert src.read_text() != original |
| 382 | + |
| 383 | + def test_single_line_copyright_passes_with_wildcard(self, tmp_path): |
| 384 | + # Existing repos often have a condensed copyright line without the full license block |
| 385 | + content = '// Copyright (c) 2025-present Polymath Robotics, Inc. All rights reserved\nint x = 0;\n' |
| 386 | + src = self._write(tmp_path, 'f.cpp', content) |
| 387 | + lf = self._license_file(tmp_path) |
| 388 | + ret = main([ |
| 389 | + '--license-filepath', |
| 390 | + lf, |
| 391 | + '--comment-style', |
| 392 | + '//', |
| 393 | + '--allow-past-years', |
| 394 | + '--wildcard-copyright-org', |
| 395 | + str(src), |
| 396 | + ]) |
| 397 | + assert ret == 0 |
| 398 | + assert src.read_text() == content # file untouched |
| 399 | + |
| 400 | + def test_single_line_copyright_with_sentinel_still_fails(self, tmp_path): |
| 401 | + content = f'// Copyright (c) 2025-present {COPYRIGHT_ORG_SENTINEL}\nint x = 0;\n' |
| 402 | + src = self._write(tmp_path, 'f.cpp', content) |
| 403 | + lf = self._license_file(tmp_path) |
| 404 | + ret = main([ |
| 405 | + '--license-filepath', |
| 406 | + lf, |
| 407 | + '--comment-style', |
| 408 | + '//', |
| 409 | + '--allow-past-years', |
| 410 | + '--wildcard-copyright-org', |
| 411 | + str(src), |
| 412 | + ]) |
| 413 | + assert ret == 1 |
| 414 | + assert src.read_text() == content # file still untouched (sentinel check doesn't modify) |
| 415 | + |
| 416 | + def test_no_copyright_at_all_inserts_sentinel(self, tmp_path): |
| 417 | + content = 'int x = 0;\n' |
| 418 | + src = self._write(tmp_path, 'f.cpp', content) |
| 419 | + lf = self._license_file(tmp_path) |
| 420 | + ret = main([ |
| 421 | + '--license-filepath', |
| 422 | + lf, |
| 423 | + '--comment-style', |
| 424 | + '//', |
| 425 | + '--allow-past-years', |
| 426 | + '--wildcard-copyright-org', |
| 427 | + str(src), |
| 428 | + ]) |
| 429 | + assert ret == 1 |
| 430 | + assert COPYRIGHT_ORG_SENTINEL in src.read_text() |
0 commit comments