|
| 1 | +import sys |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from modflow_devtools.version import get_version, set_version, update_file |
| 6 | + |
| 7 | +# --------------------------------------------------------------------------- |
| 8 | +# Fixtures |
| 9 | +# --------------------------------------------------------------------------- |
| 10 | + |
| 11 | + |
| 12 | +@pytest.fixture |
| 13 | +def project_dir(tmp_path): |
| 14 | + """A minimal project directory with version.txt, meson.build, pixi.toml.""" |
| 15 | + (tmp_path / "version.txt").write_text("1.0.0") |
| 16 | + (tmp_path / "meson.build").write_text( |
| 17 | + "project(\n 'testproj',\n version: '1.0.0',\n meson_version: '>= 1.0',\n)\n" |
| 18 | + ) |
| 19 | + (tmp_path / "pixi.toml").write_text( |
| 20 | + '[project]\nname = "testproj"\nversion = "1.0.0"\n\n[dependencies]\n' |
| 21 | + ) |
| 22 | + return tmp_path |
| 23 | + |
| 24 | + |
| 25 | +@pytest.fixture |
| 26 | +def fortran_file(tmp_path): |
| 27 | + """A file with a Fortran-style version string.""" |
| 28 | + path = tmp_path / "src" / "prog.f" |
| 29 | + path.parent.mkdir() |
| 30 | + path.write_text(" PARAMETER (VERSION='1.0.0 01/01/2020')\n") |
| 31 | + return path |
| 32 | + |
| 33 | + |
| 34 | +# --------------------------------------------------------------------------- |
| 35 | +# Unit tests: get_version |
| 36 | +# --------------------------------------------------------------------------- |
| 37 | + |
| 38 | + |
| 39 | +class TestGetVersion: |
| 40 | + def test_reads_version(self, project_dir): |
| 41 | + assert get_version(project_dir) == "1.0.0" |
| 42 | + |
| 43 | + def test_missing_version_txt(self, tmp_path): |
| 44 | + with pytest.raises(FileNotFoundError): |
| 45 | + get_version(tmp_path) |
| 46 | + |
| 47 | + def test_strips_whitespace(self, tmp_path): |
| 48 | + (tmp_path / "version.txt").write_text(" 2.3.4\n") |
| 49 | + assert get_version(tmp_path) == "2.3.4" |
| 50 | + |
| 51 | + |
| 52 | +# --------------------------------------------------------------------------- |
| 53 | +# Unit tests: _update_version_txt, _update_meson_build, _update_pixi_toml |
| 54 | +# (tested indirectly through set_version) |
| 55 | +# --------------------------------------------------------------------------- |
| 56 | + |
| 57 | + |
| 58 | +class TestSetVersion: |
| 59 | + def test_updates_all_three_files(self, project_dir): |
| 60 | + set_version("2.0.0", project_dir) |
| 61 | + assert (project_dir / "version.txt").read_text() == "2.0.0" |
| 62 | + assert "version: '2.0.0'" in (project_dir / "meson.build").read_text() |
| 63 | + assert 'version = "2.0.0"' in (project_dir / "pixi.toml").read_text() |
| 64 | + |
| 65 | + def test_does_not_modify_meson_version_line(self, project_dir): |
| 66 | + set_version("2.0.0", project_dir) |
| 67 | + meson = (project_dir / "meson.build").read_text() |
| 68 | + assert "meson_version: '>= 1.0'" in meson |
| 69 | + |
| 70 | + def test_invalid_version_raises(self, project_dir): |
| 71 | + with pytest.raises(ValueError, match="Invalid version"): |
| 72 | + set_version("not-a-version", project_dir) |
| 73 | + |
| 74 | + def test_missing_version_txt_raises(self, tmp_path): |
| 75 | + # No version.txt in tmp_path |
| 76 | + (tmp_path / "meson.build").write_text("project(\n version: '1.0.0',\n)\n") |
| 77 | + (tmp_path / "pixi.toml").write_text('[project]\nversion = "1.0.0"\n') |
| 78 | + with pytest.raises(FileNotFoundError): |
| 79 | + set_version("2.0.0", tmp_path) |
| 80 | + |
| 81 | + def test_missing_meson_build_warns(self, tmp_path, capsys): |
| 82 | + (tmp_path / "version.txt").write_text("1.0.0") |
| 83 | + (tmp_path / "pixi.toml").write_text('[project]\nversion = "1.0.0"\n') |
| 84 | + set_version("2.0.0", tmp_path) |
| 85 | + assert "meson.build" in capsys.readouterr().err |
| 86 | + |
| 87 | + def test_missing_pixi_toml_warns(self, tmp_path, capsys): |
| 88 | + (tmp_path / "version.txt").write_text("1.0.0") |
| 89 | + (tmp_path / "meson.build").write_text("project(\n version: '1.0.0',\n)\n") |
| 90 | + set_version("2.0.0", tmp_path) |
| 91 | + assert "pixi.toml" in capsys.readouterr().err |
| 92 | + |
| 93 | + |
| 94 | +# --------------------------------------------------------------------------- |
| 95 | +# Unit tests: dry_run |
| 96 | +# --------------------------------------------------------------------------- |
| 97 | + |
| 98 | + |
| 99 | +class TestDryRun: |
| 100 | + def test_no_files_modified(self, project_dir, capsys): |
| 101 | + set_version("9.9.9", project_dir, dry_run=True) |
| 102 | + assert (project_dir / "version.txt").read_text() == "1.0.0" |
| 103 | + assert "version: '1.0.0'" in (project_dir / "meson.build").read_text() |
| 104 | + assert 'version = "1.0.0"' in (project_dir / "pixi.toml").read_text() |
| 105 | + |
| 106 | + def test_prints_expected_changes(self, project_dir, capsys): |
| 107 | + set_version("9.9.9", project_dir, dry_run=True) |
| 108 | + out = capsys.readouterr().out |
| 109 | + assert "9.9.9" in out |
| 110 | + assert "1.0.0" in out |
| 111 | + |
| 112 | + |
| 113 | +# --------------------------------------------------------------------------- |
| 114 | +# Unit tests: update_file |
| 115 | +# --------------------------------------------------------------------------- |
| 116 | + |
| 117 | + |
| 118 | +class TestUpdateFile: |
| 119 | + def test_fortran_parameter_style(self, fortran_file): |
| 120 | + pattern = r"PARAMETER \(VERSION='([^']+)'\)" |
| 121 | + fmt = "PARAMETER (VERSION='{version} 06/25/2013')" |
| 122 | + update_file(fortran_file, pattern, fmt, "2.0.0") |
| 123 | + assert "PARAMETER (VERSION='2.0.0 06/25/2013')" in fortran_file.read_text() |
| 124 | + |
| 125 | + def test_provisional_suffix_preserved(self, tmp_path): |
| 126 | + f = tmp_path / "prog.f90" |
| 127 | + f.write_text(" version = '7.2.001 PROVISIONAL'\n") |
| 128 | + pattern = r"version = '([^']+)'" |
| 129 | + fmt = "version = '{version} PROVISIONAL'" |
| 130 | + update_file(f, pattern, fmt, "7.2.002") |
| 131 | + assert "version = '7.2.002 PROVISIONAL'" in f.read_text() |
| 132 | + |
| 133 | + def test_dry_run_no_modification(self, fortran_file, capsys): |
| 134 | + pattern = r"PARAMETER \(VERSION='([^']+)'\)" |
| 135 | + fmt = "PARAMETER (VERSION='{version}')" |
| 136 | + original = fortran_file.read_text() |
| 137 | + update_file(fortran_file, pattern, fmt, "9.9.9", dry_run=True) |
| 138 | + assert fortran_file.read_text() == original |
| 139 | + |
| 140 | + def test_missing_file_raises(self, tmp_path): |
| 141 | + with pytest.raises(FileNotFoundError): |
| 142 | + update_file( |
| 143 | + tmp_path / "nonexistent.f", r"VERSION='([^']+)'", "VERSION='{version}'", "1.0.0" |
| 144 | + ) |
| 145 | + |
| 146 | + def test_no_capture_group_raises(self, fortran_file): |
| 147 | + with pytest.raises(ValueError, match="exactly one capture group"): |
| 148 | + update_file( |
| 149 | + fortran_file, |
| 150 | + r"PARAMETER \(VERSION='[^']+'\)", |
| 151 | + "PARAMETER (VERSION='{version}')", |
| 152 | + "1.0.0", |
| 153 | + ) |
| 154 | + |
| 155 | + def test_multiple_capture_groups_raises(self, fortran_file): |
| 156 | + with pytest.raises(ValueError, match="exactly one capture group"): |
| 157 | + update_file(fortran_file, r"(PARAMETER) \(VERSION='([^']+)'\)", "{version}", "1.0.0") |
| 158 | + |
| 159 | + def test_format_missing_version_placeholder_raises(self, fortran_file): |
| 160 | + with pytest.raises(ValueError, match="must contain"): |
| 161 | + update_file( |
| 162 | + fortran_file, |
| 163 | + r"PARAMETER \(VERSION='([^']+)'\)", |
| 164 | + "PARAMETER (VERSION='hardcoded')", |
| 165 | + "1.0.0", |
| 166 | + ) |
| 167 | + |
| 168 | + def test_pattern_not_found_warns(self, tmp_path, capsys): |
| 169 | + f = tmp_path / "file.f" |
| 170 | + f.write_text("no version here\n") |
| 171 | + update_file(f, r"VERSION='([^']+)'", "VERSION='{version}'", "1.0.0") |
| 172 | + assert "not found" in capsys.readouterr().err |
| 173 | + |
| 174 | + |
| 175 | +# --------------------------------------------------------------------------- |
| 176 | +# Integration tests: set_version with --file/--pattern/--format |
| 177 | +# --------------------------------------------------------------------------- |
| 178 | + |
| 179 | + |
| 180 | +class TestSetVersionWithFile: |
| 181 | + def test_updates_all_files_including_fortran(self, project_dir, fortran_file): |
| 182 | + pattern = r"PARAMETER \(VERSION='([^']+)'\)" |
| 183 | + fmt = "PARAMETER (VERSION='{version}')" |
| 184 | + set_version("2.0.0", project_dir, file=fortran_file, pattern=pattern, fmt=fmt) |
| 185 | + assert (project_dir / "version.txt").read_text() == "2.0.0" |
| 186 | + assert "VERSION='2.0.0'" in fortran_file.read_text() |
| 187 | + |
| 188 | + def test_file_without_pattern_raises(self, project_dir, fortran_file): |
| 189 | + with pytest.raises(ValueError, match="--file requires"): |
| 190 | + set_version("2.0.0", project_dir, file=fortran_file, pattern=None, fmt=None) |
| 191 | + |
| 192 | + |
| 193 | +# --------------------------------------------------------------------------- |
| 194 | +# Integration tests: CLI via __main__ |
| 195 | +# --------------------------------------------------------------------------- |
| 196 | + |
| 197 | + |
| 198 | +class TestCLI: |
| 199 | + def _run(self, monkeypatch, capsys, *argv): |
| 200 | + from modflow_devtools.version.__main__ import main |
| 201 | + |
| 202 | + monkeypatch.setattr(sys, "argv", ["mf version", *argv]) |
| 203 | + try: |
| 204 | + main() |
| 205 | + except SystemExit as e: |
| 206 | + return e.code, capsys.readouterr() |
| 207 | + return 0, capsys.readouterr() |
| 208 | + |
| 209 | + def test_get(self, project_dir, monkeypatch, capsys): |
| 210 | + code, captured = self._run(monkeypatch, capsys, "get", "--root", str(project_dir)) |
| 211 | + assert code == 0 |
| 212 | + assert captured.out.strip() == "1.0.0" |
| 213 | + |
| 214 | + def test_get_root_option(self, project_dir, monkeypatch, capsys): |
| 215 | + code, captured = self._run(monkeypatch, capsys, "get", "--root", str(project_dir)) |
| 216 | + assert code == 0 |
| 217 | + assert "1.0.0" in captured.out |
| 218 | + |
| 219 | + def test_get_missing_version_txt(self, tmp_path, monkeypatch, capsys): |
| 220 | + code, captured = self._run(monkeypatch, capsys, "get", "--root", str(tmp_path)) |
| 221 | + assert code == 1 |
| 222 | + assert "Error" in captured.err |
| 223 | + |
| 224 | + def test_set(self, project_dir, monkeypatch, capsys): |
| 225 | + code, _ = self._run(monkeypatch, capsys, "set", "2.0.0", "--root", str(project_dir)) |
| 226 | + assert code == 0 |
| 227 | + assert (project_dir / "version.txt").read_text() == "2.0.0" |
| 228 | + |
| 229 | + def test_set_dry_run(self, project_dir, monkeypatch, capsys): |
| 230 | + code, captured = self._run( |
| 231 | + monkeypatch, capsys, "set", "9.9.9", "--root", str(project_dir), "--dry-run" |
| 232 | + ) |
| 233 | + assert code == 0 |
| 234 | + assert (project_dir / "version.txt").read_text() == "1.0.0" |
| 235 | + assert "9.9.9" in captured.out |
| 236 | + |
| 237 | + def test_set_with_file(self, project_dir, fortran_file, monkeypatch, capsys): |
| 238 | + code, _ = self._run( |
| 239 | + monkeypatch, |
| 240 | + capsys, |
| 241 | + "set", |
| 242 | + "2.0.0", |
| 243 | + "--root", |
| 244 | + str(project_dir), |
| 245 | + "--file", |
| 246 | + str(fortran_file), |
| 247 | + "--pattern", |
| 248 | + r"PARAMETER \(VERSION='([^']+)'\)", |
| 249 | + "--format", |
| 250 | + "PARAMETER (VERSION='{version}')", |
| 251 | + ) |
| 252 | + assert code == 0 |
| 253 | + assert "VERSION='2.0.0'" in fortran_file.read_text() |
| 254 | + |
| 255 | + def test_set_file_missing_pattern_errors(self, project_dir, fortran_file, monkeypatch, capsys): |
| 256 | + code, _ = self._run( |
| 257 | + monkeypatch, |
| 258 | + capsys, |
| 259 | + "set", |
| 260 | + "2.0.0", |
| 261 | + "--root", |
| 262 | + str(project_dir), |
| 263 | + "--file", |
| 264 | + str(fortran_file), |
| 265 | + # --pattern and --format omitted |
| 266 | + ) |
| 267 | + assert code != 0 |
| 268 | + |
| 269 | + def test_no_command_exits(self, monkeypatch, capsys): |
| 270 | + code, _ = self._run(monkeypatch, capsys) |
| 271 | + assert code == 1 |
0 commit comments