blob: 047a8029e7c8f8007e0459a5b8c7fe562222df1d [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for cipd_utils.py."""
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
import os
import subprocess
import tempfile
import textwrap
from typing import List
import unittest
from unittest import mock
import cipd_utils
def get_run_commands(mock_run) -> List[List[str]]:
"""Extracts just the subprocess command list from a mock.
This just helps reduce boilerplate in the common case where we just
want to make sure we called the right subprocess command.
For example, given these calls:
subprocess.run(["echo", "foo"], check=True, capture_output=True)
subprocess.run(["echo", "bar"], check=False)
This would return:
[["echo", "foo"], ["echo", "bar"]].
"""
return [mock_args[0] for mock_args, _ in mock_run.call_args_list]
class GitTests(unittest.TestCase):
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_git_command(self, mock_run):
git = cipd_utils.Git("path/to/repo/")
git.git(["foo", "bar"])
mock_run.assert_called_once_with(
["git", "-C", "path/to/repo/", "foo", "bar"],
check=True,
text=True,
capture_output=True)
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_changelog(self, mock_run):
git = cipd_utils.Git("path/")
mock_run.return_value = subprocess.CompletedProcess(None, 0, "fake log")
result = git.changelog("start_revision", "end_revision")
self.assertEqual(
get_run_commands(mock_run), [
[
"git", "-C", "path/", "log", "--oneline",
"start_revision..end_revision"
]
])
self.assertEqual(result, "fake log")
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_changelog_no_start(self, mock_run):
git = cipd_utils.Git("path/")
mock_run.return_value = subprocess.CompletedProcess(None, 0, "fake log")
result = git.changelog(None, "end_revision")
self.assertEqual(
get_run_commands(mock_run),
[["git", "-C", "path/", "log", "--oneline", "end_revision"]])
self.assertEqual(result, "fake log")
# Based off `repo info --local-only` output for repo version v2.21.
_FAKE_REPO_INFO = textwrap.dedent(
"""\
Manifest branch: main
Manifest merge branch: refs/heads/main
Manifest groups: all,-notdefault
----------------------------
Project: foo
Mount path: /repo/root/foo
Current revision: foo_revision
Manifest revision: main
Local Branches: 0
----------------------------
Project: bar/baz
Mount path: /repo/root/baz
Current revision: baz_revision
Manifest revision: baz_revision
Local Branches: 1 [baz_local_branch]
----------------------------
""")
class RepoTests(unittest.TestCase):
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_repo_init(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
None, 0, _FAKE_REPO_INFO)
repo = cipd_utils.Repo("/repo/root")
self.assertEqual(
get_run_commands(mock_run), [["repo", "info", "--local-only"]])
self.assertEqual(len(repo.git_repos), 2)
self.assertEqual(repo.git_repos["foo"].repo_path, "/repo/root/foo")
self.assertEqual(repo.git_repos["bar/baz"].repo_path, "/repo/root/baz")
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_repo_init_spec_no_alias(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
None, 0, _FAKE_REPO_INFO)
repo = cipd_utils.Repo("/repo/root", spec={"foo": None})
self.assertEqual(len(repo.git_repos), 1)
self.assertEqual(repo.git_repos["foo"].repo_path, "/repo/root/foo")
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_repo_init_spec_with_alias(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
None, 0, _FAKE_REPO_INFO)
repo = cipd_utils.Repo("/repo/root", spec={"baz": "baz_alias"})
self.assertEqual(len(repo.git_repos), 1)
self.assertEqual(
repo.git_repos["baz_alias"].repo_path, "/repo/root/baz")
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_repo_init_spec_unused(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
None, 0, _FAKE_REPO_INFO)
with self.assertRaises(ValueError):
cipd_utils.Repo("/repo/root", spec={"foo": None, "unknown": None})
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_repo_init_spec_name_collision(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
None, 0, _FAKE_REPO_INFO)
with self.assertRaises(ValueError):
cipd_utils.Repo(
"/repo/root", spec={
"foo": "same_alias",
"baz": "same_alias"
})
class CipdTests(unittest.TestCase):
@mock.patch.object(
cipd_utils.os.path, "isdir", autospec=True, return_value=False)
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_cipd_manifest(self, mock_run, _mock_isdir):
# The code under test will download the CIPD package into a tempdir
# so we don't know the path ahead of time. In order to inject a fake
# manifest, this function will write it to disk as a side-effect of
# the mock subprocess.run() call.
def write_cipd_manifest_side_effect(command, *args, **kwargs):
# The path to download will follow the "-root" arg.
download_root = command[command.index("-root") + 1]
with open(os.path.join(download_root, "manifest.json"), "w") as f:
f.write(
textwrap.dedent(
"""\
{
"repo1": "revision1",
"repo2": "revision2"
}
"""))
mock_run.side_effect = write_cipd_manifest_side_effect
manifest = cipd_utils.get_cipd_version_manifest("package", "version")
self.assertEqual(manifest, {"repo1": "revision1", "repo2": "revision2"})
@mock.patch.object(cipd_utils.subprocess, "run", autospec=True)
def test_cipd_manifest_local(self, mock_run):
with tempfile.TemporaryDirectory() as temp_dir:
with open(os.path.join(temp_dir, "manifest.json"), "w") as f:
f.write(
textwrap.dedent(
"""\
{
"repo1": "revision1",
"repo2": "revision2"
}
"""))
manifest = cipd_utils.get_cipd_version_manifest("package", temp_dir)
self.assertEqual(manifest, {"repo1": "revision1", "repo2": "revision2"})
# We should not have called into `cipd` at all since the package was
# available locally.
mock_run.assert_not_called()
def set_up_changelog_mocks(
mock_get_cipd_version_manifest, cipd_version_a, cipd_version_b,
repo_info):
"""Configures all the mocks needed to produce a changelog.
There's a lot of mocking that needs to be done to set up the proper
CIPD, repo, and git calls. Extracting it here allows the tests to
just focus on the logic.
Args:
mock_get_cipd_version_manifest: get_cipd_version_manifest() mock.
cipd_version_a: CIPD version A.
cipd_version_b: CIPD version B.
repo_info: a {name, info} dict of repos to mock out, where |info| is a
tuple (revision A, revision B, A->B commits, B->A commits).
Returns:
The mock Repo object to use.
"""
# Mock out the CIPD manifest for the given repos.
def cipd_version_manifest(_, cipd_version):
if cipd_version == cipd_version_a:
git_rev_index = 0
elif cipd_version == cipd_version_b:
git_rev_index = 1
else:
raise ValueError(f"Unexpected CIPD version {cipd_version}")
manifest = {}
for name, info in repo_info.items():
git_rev = info[git_rev_index]
if git_rev:
manifest[name] = git_rev
return manifest
mock_get_cipd_version_manifest.side_effect = cipd_version_manifest
# Mock out the Repo and each Git sub-object.
mock_repo = mock.Mock(spec=cipd_utils.Repo)
mock_repo.git_repos = {}
for name, info in repo_info.items():
mock_git = mock.Mock(spec=cipd_utils.Git)
# Python functions are late-binding, meaning the variables will be
# captured at call-time, not at definition. So we need a wrapper here
# that we can call immediately to bind the loop variables, or else
# they'll change on the next loop iteration.
def wrap_changelog():
rev_a, rev_b, a_to_b, b_to_a = info
def changelog(old_revision, new_revision):
if old_revision == rev_a and new_revision == rev_b:
return "\n".join(a_to_b)
elif old_revision == rev_b and new_revision == rev_a:
return "\n".join(b_to_a)
raise ValueError(
f"Unknown {name} revisions {old_revision}, {new_revision}")
return changelog
mock_git.changelog.side_effect = wrap_changelog()
mock_repo.git_repos[name] = mock_git
return mock_repo
class ChangelogTests(unittest.TestCase):
@mock.patch.object(cipd_utils, "get_cipd_version_manifest", autospec=True)
def test_changelog(self, mock_get_cipd_version_manifest):
mock_repo = set_up_changelog_mocks(
mock_get_cipd_version_manifest, "cipd_ver_A", "cipd_ver_B", {
"repo1":
[
"rev1_A",
"rev1_B",
["[repo1] commit 1", "[repo1] commit 2"],
[],
],
"repo2":
[
"rev2_A",
"rev2_B",
[
"[repo2] commit 1", "[repo2] commit 2",
"[repo2] commit 3"
],
[],
]
})
changelog = cipd_utils.changelog(
mock_repo, "package_name", "cipd_ver_A", "cipd_ver_B")
self.assertEqual(
changelog,
textwrap.dedent(
"""\
-- Changelist --
repo1:
[repo1] commit 1
[repo1] commit 2
repo2:
[repo2] commit 1
[repo2] commit 2
[repo2] commit 3
-- Source Revisions --
repo1: rev1_B
repo2: rev2_B"""))
@mock.patch.object(cipd_utils, "get_cipd_version_manifest", autospec=True)
def test_changelog_new_repo(self, mock_get_cipd_version_manifest):
mock_repo = set_up_changelog_mocks(
mock_get_cipd_version_manifest,
"cipd_ver_A",
"cipd_ver_B",
{
"repo1":
[
None, # Repo does not exist in the first version.
"rev1_B",
["[repo1] commit 1", "[repo1] commit 2"],
[],
],
})
changelog = cipd_utils.changelog(
mock_repo, "package_name", "cipd_ver_A", "cipd_ver_B")
self.assertEqual(
changelog,
textwrap.dedent(
"""\
-- Changelist --
repo1:
[repo1] commit 1
[repo1] commit 2
-- Source Revisions --
repo1: rev1_B"""))
@mock.patch.object(cipd_utils, "get_cipd_version_manifest", autospec=True)
def test_changelog_deleted_repo(self, mock_get_cipd_version_manifest):
mock_repo = set_up_changelog_mocks(
mock_get_cipd_version_manifest,
"cipd_ver_A",
"cipd_ver_B",
{
"repo1":
[
"rev1_A",
"rev1_B",
["[repo1] commit 1", "[repo1] commit 2"],
[],
],
"repo2":
[
"rev2_A",
None, # Repo does not exist in the second version.
["[repo2] this should not be printed"],
["[repo2] this should not be printed either"],
]
})
changelog = cipd_utils.changelog(
mock_repo, "package_name", "cipd_ver_A", "cipd_ver_B")
self.assertEqual(
changelog,
textwrap.dedent(
"""\
-- Changelist --
repo1:
[repo1] commit 1
[repo1] commit 2
repo2:
[removed commits:]
[repo has been removed]
-- Source Revisions --
repo1: rev1_B"""))
@mock.patch.object(cipd_utils, "get_cipd_version_manifest", autospec=True)
def test_changelog_removed_commits(self, mock_get_cipd_version_manifest):
mock_repo = set_up_changelog_mocks(
mock_get_cipd_version_manifest,
"cipd_ver_A",
"cipd_ver_B",
{
"repo1":
[
"rev1_A",
"rev1_B",
["[repo1] commit 1", "[repo1] commit 2"],
# A is not a direct ancestor of B - it had a commit that
# no longer exists in the new version.
["[repo1] removed commit"],
]
})
changelog = cipd_utils.changelog(
mock_repo, "package_name", "cipd_ver_A", "cipd_ver_B")
self.assertEqual(
changelog,
textwrap.dedent(
"""\
-- Changelist --
repo1:
[repo1] commit 1
[repo1] commit 2
[removed commits:]
[repo1] removed commit
-- Source Revisions --
repo1: rev1_B"""))
@mock.patch.object(cipd_utils, "get_cipd_version_manifest", autospec=True)
def test_changelog_no_changes(self, mock_get_cipd_version_manifest):
mock_repo = set_up_changelog_mocks(
mock_get_cipd_version_manifest, "cipd_ver_A", "cipd_ver_B",
{"repo1": [
"rev1_no_change",
"rev1_no_change",
[],
[],
]})
changelog = cipd_utils.changelog(
mock_repo, "package_name", "cipd_ver_A", "cipd_ver_B")
self.assertEqual(
changelog,
textwrap.dedent(
"""\
-- Changelist --
[no changes]
-- Source Revisions --
repo1: rev1_no_change"""))
if __name__ == "__main__":
unittest.main()