/ tests / dev / test_update_mlflow_versions.py
test_update_mlflow_versions.py
  1  import difflib
  2  import re
  3  from pathlib import Path
  4  
  5  import pytest
  6  from packaging.version import Version
  7  
  8  from dev.update_mlflow_versions import (
  9      get_current_py_version,
 10      replace_java,
 11      replace_java_pom_xml,
 12      replace_pyproject_toml,
 13      replace_python,
 14      replace_r,
 15      replace_ts,
 16  )
 17  
 18  # { filename: expected lines changed }
 19  _JAVA_FILES = {}
 20  
 21  _JAVA_XML_FILES = {
 22      "mlflow/java/pom.xml": {
 23          6: "  <version>{new_version}</version>",
 24          52: "    <mlflow-version>{new_version}</mlflow-version>",
 25      },
 26      "mlflow/java/client/pom.xml": {
 27          8: "    <version>{new_version}</version>",
 28      },
 29      "mlflow/java/spark_2.12/pom.xml": {
 30          4: "  <version>{new_version}</version>",
 31          18: "    <version>{new_version}</version>",
 32      },
 33      "mlflow/java/spark_2.13/pom.xml": {
 34          4: "  <version>{new_version}</version>",
 35          18: "    <version>{new_version}</version>",
 36      },
 37  }
 38  
 39  _TS_FILES = {
 40      "mlflow/server/js/src/common/constants.tsx": {
 41          12: "export const Version = '{new_version}';",
 42      },
 43      "docs/src/constants.ts": {
 44          1: "export const Version = '{new_version}';",
 45      },
 46  }
 47  
 48  _PYTHON_FILES = {
 49      "mlflow/version.py": {
 50          5: 'VERSION = "{new_version}"',
 51      }
 52  }
 53  
 54  _PYPROJECT_TOML_FILES = {
 55      "pyproject.toml": {
 56          12: 'version = "{new_version}"',
 57      },
 58      "pyproject.release.toml": {
 59          12: 'version = "{new_version}"',
 60          30: '  "mlflow-skinny=={new_version}",',
 61          31: '  "mlflow-tracing=={new_version}",',
 62      },
 63      "libs/skinny/pyproject.toml": {
 64          10: 'version = "{new_version}"',
 65      },
 66      "libs/tracing/pyproject.toml": {
 67          10: 'version = "{new_version}"',
 68      },
 69  }
 70  
 71  _R_FILES = {
 72      "mlflow/R/mlflow/DESCRIPTION": {
 73          4: "Version: {new_version}",
 74      }
 75  }
 76  
 77  _DIFF_REGEX = re.compile(r"--- (\d+,?\d*) ----")
 78  
 79  old_version = Version(get_current_py_version())
 80  _NEW_PY_VERSION = f"{old_version.major}.{old_version.minor}.{old_version.micro + 1}"
 81  
 82  
 83  def copy_and_run_change_func(monkeypatch, tmp_path, paths_to_copy, replace_func, new_version):
 84      for path in paths_to_copy:
 85          copy_path = tmp_path / path
 86          copy_path.parent.mkdir(parents=True, exist_ok=True)
 87          copy_path.write_text(path.read_text())
 88  
 89      with monkeypatch.context() as m:
 90          m.chdir(tmp_path)
 91  
 92          # pyproject.toml replace doesn't search for the old version,
 93          # it just replaces the version line with the new version.
 94          if replace_func == replace_pyproject_toml:
 95              replace_func(new_version, paths_to_copy)
 96          else:
 97              replace_func(str(old_version), new_version, paths_to_copy)
 98  
 99  
100  @pytest.mark.parametrize(
101      ("replace_func", "expect_dict", "new_py_version", "expected_new_version"),
102      [
103          (replace_java, _JAVA_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
104          (replace_java, _JAVA_FILES, _NEW_PY_VERSION + ".dev0", _NEW_PY_VERSION + "-SNAPSHOT"),
105          (replace_java, _JAVA_FILES, _NEW_PY_VERSION + "rc1", _NEW_PY_VERSION + "-SNAPSHOT"),
106          (replace_java_pom_xml, _JAVA_XML_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
107          (
108              replace_java_pom_xml,
109              _JAVA_XML_FILES,
110              _NEW_PY_VERSION + ".dev0",
111              _NEW_PY_VERSION + "-SNAPSHOT",
112          ),
113          (
114              replace_java_pom_xml,
115              _JAVA_XML_FILES,
116              _NEW_PY_VERSION + "rc1",
117              _NEW_PY_VERSION + "-SNAPSHOT",
118          ),
119          (replace_ts, _TS_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
120          (replace_python, _PYTHON_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
121          (replace_pyproject_toml, _PYPROJECT_TOML_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
122          (replace_r, _R_FILES, _NEW_PY_VERSION, _NEW_PY_VERSION),
123      ],
124  )
125  def test_update_mlflow_versions(
126      monkeypatch, tmp_path, replace_func, expect_dict, new_py_version, expected_new_version
127  ):
128      paths_to_change = [Path(filename) for filename in expect_dict]
129      copy_and_run_change_func(
130          monkeypatch,
131          tmp_path,
132          # always copy version.py since we need it in get_current_py_version()
133          paths_to_change + [Path("mlflow/version.py")],
134          replace_func,
135          new_py_version,
136      )
137  
138      # diff files
139      for filename, expected_changes in expect_dict.items():
140          old_file = Path(filename).read_text().splitlines()
141          new_file = (tmp_path / filename).read_text().splitlines()
142          diff = list(difflib.context_diff(old_file, new_file, n=0))
143          changed_lines = _parse_diff_line(diff)
144  
145          formatted_expected_changes = {
146              line_num: change.format(new_version=expected_new_version)
147              for line_num, change in expected_changes.items()
148          }
149  
150          assert changed_lines == formatted_expected_changes
151  
152  
153  def _parse_diff_line(diff: list[str]) -> dict[int, str]:
154      diff_lines = {}
155      for idx, line in enumerate(diff):
156          match = _DIFF_REGEX.search(line)
157          if not match:
158              continue
159  
160          if "," in match.group(1):
161              # multi-line change is represented as [(start,end), line1, line2, ...]
162              start, end = map(int, match.group(1).split(","))
163              for i in range(start, end + 1):
164                  # the [2:] is to cut out the "! " at the beginning of diff lines
165                  diff_lines[i] = diff[idx + (i - start) + 1][2:]
166          else:
167              # single-line change
168              diff_lines[int(match.group(1))] = diff[idx + 1][2:]
169  
170      return diff_lines