|
10 | 10 | VersionArgs, |
11 | 11 | VersionChoice, |
12 | 12 | _apply_version_choice, |
| 13 | + _find_cargo_lock_for, |
| 14 | + _get_cargo_package_name, |
13 | 15 | _get_default_bump_from_commits, |
14 | 16 | _handle_explicit_version_args, |
15 | 17 | _handle_interactive_version_selection, |
|
37 | 39 | get_version_from_version_txt, |
38 | 40 | handle_version, |
39 | 41 | show_file_diff, |
| 42 | + update_cargo_lock_version, |
40 | 43 | update_cargo_toml_version, |
41 | 44 | update_file, |
42 | 45 | update_version_files, |
@@ -941,6 +944,239 @@ def test_update_cargo_toml_version_file_not_exists(self, tmp_path): |
941 | 944 | update_cargo_toml_version(non_existent_file, "1.0.0", 0, show_diff=False) |
942 | 945 |
|
943 | 946 |
|
| 947 | +class TestGetCargoPackageName: |
| 948 | + """Test cases for _get_cargo_package_name.""" |
| 949 | + |
| 950 | + def test_basic(self, tmp_path): |
| 951 | + cargo_toml = tmp_path / "Cargo.toml" |
| 952 | + cargo_toml.write_text('[package]\nname = "arthash"\nversion = "0.2.0"\n') |
| 953 | + assert _get_cargo_package_name(cargo_toml) == "arthash" |
| 954 | + |
| 955 | + def test_missing_file(self, tmp_path): |
| 956 | + assert _get_cargo_package_name(tmp_path / "Cargo.toml") is None |
| 957 | + |
| 958 | + def test_missing_package_section(self, tmp_path): |
| 959 | + cargo_toml = tmp_path / "Cargo.toml" |
| 960 | + cargo_toml.write_text('[workspace]\nmembers = ["a"]\n') |
| 961 | + assert _get_cargo_package_name(cargo_toml) is None |
| 962 | + |
| 963 | + def test_missing_name_key(self, tmp_path): |
| 964 | + cargo_toml = tmp_path / "Cargo.toml" |
| 965 | + cargo_toml.write_text('[package]\nversion = "0.1.0"\n') |
| 966 | + assert _get_cargo_package_name(cargo_toml) is None |
| 967 | + |
| 968 | + def test_empty_name(self, tmp_path): |
| 969 | + cargo_toml = tmp_path / "Cargo.toml" |
| 970 | + cargo_toml.write_text('[package]\nname = ""\nversion = "0.1.0"\n') |
| 971 | + assert _get_cargo_package_name(cargo_toml) is None |
| 972 | + |
| 973 | + def test_invalid_toml(self, tmp_path): |
| 974 | + cargo_toml = tmp_path / "Cargo.toml" |
| 975 | + cargo_toml.write_text("this is not valid toml [[[") |
| 976 | + assert _get_cargo_package_name(cargo_toml) is None |
| 977 | + |
| 978 | + |
| 979 | +class TestFindCargoLockFor: |
| 980 | + """Test cases for _find_cargo_lock_for.""" |
| 981 | + |
| 982 | + def test_same_directory(self, tmp_path): |
| 983 | + """Single-crate layout: Cargo.toml and Cargo.lock side-by-side.""" |
| 984 | + (tmp_path / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n') |
| 985 | + lockfile = tmp_path / "Cargo.lock" |
| 986 | + lockfile.write_text("# lock\n") |
| 987 | + assert _find_cargo_lock_for(tmp_path) == lockfile |
| 988 | + |
| 989 | + def test_parent_directory(self, tmp_path): |
| 990 | + """Workspace layout: lockfile lives at workspace root.""" |
| 991 | + (tmp_path / ".git").mkdir() |
| 992 | + lockfile = tmp_path / "Cargo.lock" |
| 993 | + lockfile.write_text("# lock\n") |
| 994 | + member = tmp_path / "crates" / "alpha" |
| 995 | + member.mkdir(parents=True) |
| 996 | + (member / "Cargo.toml").write_text('[package]\nname = "alpha"\nversion = "0.1.0"\n') |
| 997 | + assert _find_cargo_lock_for(member) == lockfile |
| 998 | + |
| 999 | + def test_no_lockfile_returns_none(self, tmp_path): |
| 1000 | + (tmp_path / ".git").mkdir() |
| 1001 | + crate = tmp_path / "crate" |
| 1002 | + crate.mkdir() |
| 1003 | + (crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n') |
| 1004 | + assert _find_cargo_lock_for(crate) is None |
| 1005 | + |
| 1006 | + def test_stops_at_git_root(self, tmp_path): |
| 1007 | + """Must not escape the current repo, even if an outer Cargo.lock exists.""" |
| 1008 | + outer_lock = tmp_path / "Cargo.lock" |
| 1009 | + outer_lock.write_text("# outer\n") |
| 1010 | + repo = tmp_path / "inner_repo" |
| 1011 | + repo.mkdir() |
| 1012 | + (repo / ".git").mkdir() |
| 1013 | + crate = repo / "crate" |
| 1014 | + crate.mkdir() |
| 1015 | + (crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n') |
| 1016 | + # Should NOT find the outer Cargo.lock — that's in a different repo. |
| 1017 | + assert _find_cargo_lock_for(crate) is None |
| 1018 | + |
| 1019 | + |
| 1020 | +class TestUpdateCargoLockVersion: |
| 1021 | + """Test cases for update_cargo_lock_version.""" |
| 1022 | + |
| 1023 | + @staticmethod |
| 1024 | + def _make_lockfile(tmp_path: Path, content: str) -> Path: |
| 1025 | + lockfile = tmp_path / "Cargo.lock" |
| 1026 | + lockfile.write_text(content, encoding="utf-8") |
| 1027 | + return lockfile |
| 1028 | + |
| 1029 | + def test_local_crate_version_updated(self, tmp_path): |
| 1030 | + """Workspace member (no source field) gets its version rewritten.""" |
| 1031 | + content = ( |
| 1032 | + "# This file is automatically @generated by Cargo.\n" |
| 1033 | + "version = 4\n\n" |
| 1034 | + "[[package]]\n" |
| 1035 | + 'name = "arthash"\n' |
| 1036 | + 'version = "0.2.0"\n' |
| 1037 | + "dependencies = [\n" |
| 1038 | + ' "matrixmultiply",\n' |
| 1039 | + "]\n" |
| 1040 | + ) |
| 1041 | + lockfile = self._make_lockfile(tmp_path, content) |
| 1042 | + update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False) |
| 1043 | + new_content = lockfile.read_text() |
| 1044 | + assert 'version = "0.3.0"' in new_content |
| 1045 | + assert 'version = "0.2.0"' not in new_content |
| 1046 | + |
| 1047 | + def test_registry_dep_with_same_name_not_touched(self, tmp_path): |
| 1048 | + """A registry dep named identically to the local crate must NOT be hit. |
| 1049 | +
|
| 1050 | + Registry entries have `source = "registry+..."` between `name` and |
| 1051 | + `version`, so they fail the "name immediately followed by version" |
| 1052 | + anchor. Local crates fit the anchor exactly. |
| 1053 | + """ |
| 1054 | + content = ( |
| 1055 | + "[[package]]\n" |
| 1056 | + 'name = "arthash"\n' |
| 1057 | + 'source = "registry+https://github.com/rust-lang/crates.io-index"\n' |
| 1058 | + 'version = "9.9.9"\n' |
| 1059 | + 'checksum = "deadbeef"\n\n' |
| 1060 | + "[[package]]\n" |
| 1061 | + 'name = "arthash"\n' |
| 1062 | + 'version = "0.2.0"\n' |
| 1063 | + "dependencies = [\n" |
| 1064 | + ' "matrixmultiply",\n' |
| 1065 | + "]\n" |
| 1066 | + ) |
| 1067 | + lockfile = self._make_lockfile(tmp_path, content) |
| 1068 | + update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False) |
| 1069 | + new_content = lockfile.read_text() |
| 1070 | + # registry entry untouched |
| 1071 | + assert 'version = "9.9.9"' in new_content |
| 1072 | + # local entry bumped |
| 1073 | + assert 'version = "0.3.0"' in new_content |
| 1074 | + # only one occurrence of the new version (didn't accidentally double-write) |
| 1075 | + assert new_content.count('version = "0.3.0"') == 1 |
| 1076 | + |
| 1077 | + def test_crate_not_in_lockfile_is_noop(self, tmp_path): |
| 1078 | + """If the crate isn't recorded, nothing changes.""" |
| 1079 | + content = ( |
| 1080 | + "[[package]]\n" |
| 1081 | + 'name = "other"\n' |
| 1082 | + 'version = "1.0.0"\n' |
| 1083 | + ) |
| 1084 | + lockfile = self._make_lockfile(tmp_path, content) |
| 1085 | + update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False) |
| 1086 | + assert lockfile.read_text() == content |
| 1087 | + |
| 1088 | + def test_missing_lockfile_is_noop(self, tmp_path): |
| 1089 | + """Pure non-Rust projects don't have a Cargo.lock — must not error.""" |
| 1090 | + lockfile = tmp_path / "Cargo.lock" |
| 1091 | + # Should not raise. |
| 1092 | + update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False) |
| 1093 | + assert not lockfile.exists() |
| 1094 | + |
| 1095 | + def test_already_in_sync_is_noop(self, tmp_path): |
| 1096 | + """Idempotent: same version in -> file unchanged.""" |
| 1097 | + content = ( |
| 1098 | + "[[package]]\n" |
| 1099 | + 'name = "arthash"\n' |
| 1100 | + 'version = "0.3.0"\n' |
| 1101 | + ) |
| 1102 | + lockfile = self._make_lockfile(tmp_path, content) |
| 1103 | + before = lockfile.stat().st_mtime_ns |
| 1104 | + update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False) |
| 1105 | + # Content identical; we explicitly skip the write in that case. |
| 1106 | + assert lockfile.read_text() == content |
| 1107 | + # And the file was not rewritten (mtime preserved). Some filesystems |
| 1108 | + # have coarse mtime resolution, so this guard is a tightening only — |
| 1109 | + # the content check above is the real assertion. |
| 1110 | + assert lockfile.stat().st_mtime_ns == before |
| 1111 | + |
| 1112 | + |
| 1113 | +class TestUpdateVersionInFileCargoIntegration: |
| 1114 | + """update_version_in_file should sync Cargo.lock when it sees Cargo.toml.""" |
| 1115 | + |
| 1116 | + def test_cargo_toml_branch_syncs_sibling_lockfile(self, tmp_path): |
| 1117 | + cargo_toml = tmp_path / "Cargo.toml" |
| 1118 | + cargo_toml.write_text( |
| 1119 | + "[package]\n" |
| 1120 | + 'name = "arthash"\n' |
| 1121 | + 'version = "0.2.0"\n' |
| 1122 | + "\n" |
| 1123 | + "[dependencies]\n" |
| 1124 | + 'matrixmultiply = "0.3"\n', |
| 1125 | + encoding="utf-8", |
| 1126 | + ) |
| 1127 | + lockfile = tmp_path / "Cargo.lock" |
| 1128 | + lockfile.write_text( |
| 1129 | + "[[package]]\n" |
| 1130 | + 'name = "arthash"\n' |
| 1131 | + 'version = "0.2.0"\n' |
| 1132 | + "dependencies = [\n" |
| 1133 | + ' "matrixmultiply",\n' |
| 1134 | + "]\n", |
| 1135 | + encoding="utf-8", |
| 1136 | + ) |
| 1137 | + update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False) |
| 1138 | + # Both files should now report 0.3.0. |
| 1139 | + assert 'version = "0.3.0"' in cargo_toml.read_text() |
| 1140 | + assert 'version = "0.3.0"' in lockfile.read_text() |
| 1141 | + |
| 1142 | + def test_cargo_toml_branch_syncs_workspace_root_lockfile(self, tmp_path): |
| 1143 | + """Lockfile in workspace root, Cargo.toml in a member directory.""" |
| 1144 | + (tmp_path / ".git").mkdir() |
| 1145 | + lockfile = tmp_path / "Cargo.lock" |
| 1146 | + lockfile.write_text( |
| 1147 | + "[[package]]\n" |
| 1148 | + 'name = "alpha"\n' |
| 1149 | + 'version = "0.2.0"\n', |
| 1150 | + encoding="utf-8", |
| 1151 | + ) |
| 1152 | + member_dir = tmp_path / "crates" / "alpha" |
| 1153 | + member_dir.mkdir(parents=True) |
| 1154 | + cargo_toml = member_dir / "Cargo.toml" |
| 1155 | + cargo_toml.write_text( |
| 1156 | + "[package]\n" |
| 1157 | + 'name = "alpha"\n' |
| 1158 | + 'version = "0.2.0"\n', |
| 1159 | + encoding="utf-8", |
| 1160 | + ) |
| 1161 | + update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False) |
| 1162 | + assert 'version = "0.3.0"' in cargo_toml.read_text() |
| 1163 | + assert 'version = "0.3.0"' in lockfile.read_text() |
| 1164 | + |
| 1165 | + def test_cargo_toml_branch_no_lockfile_present(self, tmp_path): |
| 1166 | + """When there's no lockfile at all, the manifest update still proceeds.""" |
| 1167 | + (tmp_path / ".git").mkdir() |
| 1168 | + cargo_toml = tmp_path / "Cargo.toml" |
| 1169 | + cargo_toml.write_text( |
| 1170 | + "[package]\n" |
| 1171 | + 'name = "alpha"\n' |
| 1172 | + 'version = "0.2.0"\n', |
| 1173 | + encoding="utf-8", |
| 1174 | + ) |
| 1175 | + # Should not raise. |
| 1176 | + update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False) |
| 1177 | + assert 'version = "0.3.0"' in cargo_toml.read_text() |
| 1178 | + |
| 1179 | + |
944 | 1180 | class TestParseGitignore: |
945 | 1181 | """Test cases for _parse_gitignore function.""" |
946 | 1182 |
|
|
0 commit comments