async def test_analyzer(tmp_path: Path, session: ClientSession) -> None: repo = setup_python_repo(tmp_path) actor = Actor("Someone", "*****@*****.**") factory = Factory(Configuration(), session) analyzer = factory.create_python_analyzer(tmp_path) results = await analyzer.analyze() assert results == [ PythonFrozenUpdate(path=tmp_path / "requirements", applied=False) ] # Ensure that the tree is restored to the previous contents. assert not repo.is_dirty() # If the repo is dirty, analysis will fail. subprocess.run(["make", "update-deps"], cwd=str(tmp_path), check=True) assert repo.is_dirty() factory = Factory(Configuration(), session) analyzer = factory.create_python_analyzer(tmp_path) with pytest.raises(UncommittedChangesError): results = await analyzer.analyze() # Commit the changed dependencies and remove the pre-commit configuration # file. Analysis should now return no changes. repo.index.add(str(tmp_path / "requirements")) repo.index.commit("Update dependencies", author=actor, committer=actor) factory = Factory(Configuration(), session) analyzer = factory.create_python_analyzer(tmp_path) results = await analyzer.analyze() assert results == []
def main(ctx: click.Context, config_path: str) -> None: """Command-line interface for neophile.""" ctx.ensure_object(dict) assert isinstance(ctx.obj, dict) if os.path.exists(config_path): ctx.obj["config"] = Configuration.from_file(config_path) else: ctx.obj["config"] = Configuration()
async def test_pr_push_failure(tmp_path: Path, session: ClientSession) -> None: setup_repo(tmp_path) config = Configuration(github_user="******", github_token="some-token") update = HelmUpdate( path=tmp_path / "Chart.yaml", applied=False, name="gafaelfawr", current="1.0.0", latest="2.0.0", ) push_error = PushInfo(PushInfo.ERROR, None, "", None, summary="Some error") user = {"name": "Someone", "email": "*****@*****.**"} with aioresponses() as mock_responses: mock_responses.get("https://api.github.com/user", payload=user) mock_responses.get( "https://api.github.com/repos/foo/bar", payload={"default_branch": "master"}, ) pattern = re.compile( r"https://api.github.com/repos/foo/bar/pulls\?.*base=master.*") mock_responses.get(pattern, payload=[]) pr = PullRequester(tmp_path, config, session) with patch.object(Remote, "push") as mock: mock.return_value = [push_error] with pytest.raises(PushError) as excinfo: await pr.make_pull_request([update]) assert "Some error" in str(excinfo.value)
async def test_analyzer(session: ClientSession) -> None: data_path = Path(__file__).parent.parent / "data" / "kubernetes" with aioresponses() as mock: register_mock_github_tags( mock, "lsst-sqre", "sqrbot-jr", ["0.6.0", "0.6.1", "0.7.0"] ) register_mock_github_tags( mock, "lsst-sqre", "sqrbot", ["20170114", "0.6.1", "0.7.0"] ) factory = Factory(Configuration(), session) analyzer = factory.create_kustomize_analyzer(data_path) results = await analyzer.analyze() assert results == [ KustomizeUpdate( path=data_path / "sqrbot-jr" / "kustomization.yaml", applied=False, url="github.com/lsst-sqre/sqrbot-jr.git//manifests/base?ref=0.6.0", owner="lsst-sqre", repo="sqrbot-jr", current="0.6.0", latest="0.7.0", ), ]
async def test_inventory_missing(session: ClientSession) -> None: """Missing and empty version lists should return None.""" with aioresponses() as mock: register_mock_github_tags(mock, "foo", "bar", []) inventory = GitHubInventory(Configuration(), session) assert await inventory.inventory("foo", "bar") is None assert await inventory.inventory("foo", "nonexistent") is None
async def test_inventory(session: ClientSession) -> None: tests = [ { "tags": ["3.7.0", "3.8.0", "3.9.0", "3.8.1"], "latest": "3.9.0" }, { "tags": ["v3.1.0", "v3.0.1", "v3.0.0", "v2.5.0"], "latest": "v3.1.0" }, { "tags": ["4.3.20", "4.3.21-2"], "latest": "4.3.21-2" }, { "tags": ["19.10b0", "19.3b0", "18.4a4"], "latest": "19.10b0" }, ] for test in tests: with aioresponses() as mock: register_mock_github_tags(mock, "foo", "bar", test["tags"]) inventory = GitHubInventory(Configuration(), session) latest = await inventory.inventory("foo", "bar") assert latest == test["latest"]
async def test_no_updates(tmp_path: Path, session: ClientSession) -> None: data_path = Path(__file__).parent / "data" / "kubernetes" / "sqrbot-jr" tmp_repo_path = tmp_path / "tmp" tmp_repo_path.mkdir() tmp_repo = Repo.init(str(tmp_repo_path)) shutil.copytree(str(data_path), str(tmp_repo_path / "sqrbot-jr")) actor = Actor("Someone", "*****@*****.**") tmp_repo.index.commit("Initial commit", author=actor, committer=actor) upstream_path = tmp_path / "upstream" create_upstream_git_repository(tmp_repo, upstream_path) config = Configuration( repositories=[GitHubRepository(owner="foo", repo="bar")], work_area=tmp_path / "work", ) user = {"name": "Someone", "email": "*****@*****.**"} # Don't register any GitHub tag lists, so we shouldn't see any updates. with aioresponses() as mock: mock.get("https://api.github.com/user", payload=user) factory = Factory(config, session) processor = factory.create_processor() with patch_clone_from("foo", "bar", upstream_path): with patch.object(Remote, "push") as mock_push: await processor.process() assert mock_push.call_count == 0 repo = Repo(str(tmp_path / "work" / "bar")) assert not repo.is_dirty() assert repo.head.ref.name == "master"
async def test_analyzer_missing(session: ClientSession) -> None: """Test missing GitHub tags for all resources.""" data_path = Path(__file__).parent.parent / "data" / "kubernetes" with aioresponses(): factory = Factory(Configuration(), session) analyzer = factory.create_kustomize_analyzer(data_path) assert await analyzer.analyze() == []
async def test_virtualenv(tmp_path: Path, session: ClientSession, caplog: LogCaptureFixture) -> None: setup_python_repo(tmp_path, require_venv=True) factory = Factory(Configuration(), session) analyzer = factory.create_python_analyzer(tmp_path) assert await analyzer.analyze() == [] assert "make update-deps failed" in caplog.records[0].msg
async def test_pr_update(tmp_path: Path, session: ClientSession, mock_push: Mock) -> None: """Test updating an existing PR.""" repo = setup_repo(tmp_path) config = Configuration( github_email="*****@*****.**", github_token="some-token", github_user="******", ) update = HelmUpdate( path=tmp_path / "Chart.yaml", applied=False, name="gafaelfawr", current="1.0.0", latest="2.0.0", ) user = {"name": "Someone", "email": "*****@*****.**"} updated_pr = False def check_pr_update(url: str, **kwargs: Any) -> CallbackResult: change = "Update gafaelfawr Helm chart from 1.0.0 to 2.0.0" assert json.loads(kwargs["data"]) == { "title": CommitMessage.title, "body": f"- {change}\n", } nonlocal updated_pr updated_pr = True return CallbackResult(status=200) with aioresponses() as mock_responses: mock_responses.get("https://api.github.com/user", payload=user) mock_responses.get("https://api.github.com/repos/foo/bar", payload={}) pattern = re.compile( r"https://api.github.com/repos/foo/bar/pulls\?.*base=master.*") mock_responses.get(pattern, payload=[{"number": 1234}]) mock_responses.patch( "https://api.github.com/repos/foo/bar/pulls/1234", callback=check_pr_update, ) repository = Repository(tmp_path) repository.switch_branch() update.apply() pr = PullRequester(tmp_path, config, session) await pr.make_pull_request([update]) assert mock_push.call_args_list == [ call("u/neophile:u/neophile", force=True) ] assert not repo.is_dirty() assert repo.head.ref.name == "u/neophile" commit = repo.head.commit assert commit.author.name == "Someone" assert commit.author.email == "*****@*****.**" assert commit.committer.name == "Someone" assert commit.committer.email == "*****@*****.**"
async def test_inventory_semantic(session: ClientSession) -> None: tags = ["1.19.0", "1.18.0", "1.15.1", "20171120-1"] with aioresponses() as mock: register_mock_github_tags(mock, "foo", "bar", tags) inventory = GitHubInventory(Configuration(), session) latest = await inventory.inventory("foo", "bar") assert latest == "20171120-1" latest = await inventory.inventory("foo", "bar", semantic=True) assert latest == "1.19.0"
async def test_analyzer_update(tmp_path: Path, session: ClientSession) -> None: repo = setup_python_repo(tmp_path) factory = Factory(Configuration(), session) analyzer = factory.create_python_analyzer(tmp_path) results = await analyzer.update() assert results == [ PythonFrozenUpdate(path=tmp_path / "requirements", applied=True) ] assert repo.is_dirty()
async def test_get_github_repo(tmp_path: Path, session: ClientSession) -> None: repo = Repo.init(str(tmp_path)) config = Configuration(github_user="******", github_token="some-token") pr = PullRequester(tmp_path, config, session) remote = Remote.create(repo, "origin", "[email protected]:foo/bar.git") assert pr._get_github_repo() == GitHubRepository(owner="foo", repo="bar") remote.set_url("https://github.com/foo/bar.git") assert pr._get_github_repo() == GitHubRepository(owner="foo", repo="bar") remote.set_url("ssh://[email protected]/foo/bar") assert pr._get_github_repo() == GitHubRepository(owner="foo", repo="bar")
async def test_pr(tmp_path: Path, session: ClientSession, mock_push: Mock) -> None: repo = setup_repo(tmp_path) config = Configuration(github_user="******", github_token="some-token") update = HelmUpdate( path=tmp_path / "Chart.yaml", applied=False, name="gafaelfawr", current="1.0.0", latest="2.0.0", ) payload = {"name": "Someone", "email": "*****@*****.**"} with aioresponses() as mock_responses: mock_responses.get("https://api.github.com/user", payload=payload) mock_responses.get( "https://api.github.com/repos/foo/bar", payload={"default_branch": "main"}, ) pattern = re.compile( r"https://api.github.com/repos/foo/bar/pulls\?.*base=main.*") mock_responses.get(pattern, payload=[]) mock_responses.post( "https://api.github.com/repos/foo/bar/pulls", payload={}, status=201, ) repository = Repository(tmp_path) repository.switch_branch() update.apply() pr = PullRequester(tmp_path, config, session) await pr.make_pull_request([update]) assert mock_push.call_args_list == [ call("u/neophile:u/neophile", force=True) ] assert not repo.is_dirty() assert repo.head.ref.name == "u/neophile" commit = repo.head.commit assert commit.author.name == "Someone" assert commit.author.email == "*****@*****.**" assert commit.committer.name == "Someone" assert commit.committer.email == "*****@*****.**" change = "Update gafaelfawr Helm chart from 1.0.0 to 2.0.0" assert commit.message == f"{CommitMessage.title}\n\n- {change}\n" assert "tmp-neophile" not in [r.name for r in repo.remotes]
async def test_get_authenticated_remote(tmp_path: Path, session: ClientSession) -> None: repo = Repo.init(str(tmp_path)) config = Configuration(github_user="******", github_token="some-token") pr = PullRequester(tmp_path, config, session) remote = Remote.create(repo, "origin", "https://github.com/foo/bar") url = pr._get_authenticated_remote() assert url == "https://*****:*****@github.com/foo/bar" remote.set_url("https://[email protected]:8080/foo/bar") url = pr._get_authenticated_remote() assert url == "https://*****:*****@github.com:8080/foo/bar" remote.set_url("[email protected]:bar/foo") url = pr._get_authenticated_remote() assert url == "https://*****:*****@github.com/bar/foo" remote.set_url("ssh://*****:*****@github.com/baz/stuff") url = pr._get_authenticated_remote() assert url == "https://*****:*****@github.com/baz/stuff"
async def test_processor(tmp_path: Path, session: ClientSession) -> None: tmp_repo = setup_python_repo(tmp_path / "tmp", require_venv=True) upstream_path = tmp_path / "upstream" create_upstream_git_repository(tmp_repo, upstream_path) config = Configuration( repositories=[GitHubRepository(owner="foo", repo="bar")], work_area=tmp_path / "work", ) user = {"name": "Someone", "email": "*****@*****.**"} push_result = [PushInfo(PushInfo.NEW_HEAD, None, "", None)] created_pr = False def check_pr_post(url: str, **kwargs: Any) -> CallbackResult: changes = [ "Update frozen Python dependencies", "Update ambv/black pre-commit hook from 19.10b0 to 20.0.0", ] body = "- " + "\n- ".join(changes) + "\n" assert json.loads(kwargs["data"]) == { "title": CommitMessage.title, "body": body, "head": "u/neophile", "base": "main", "maintainer_can_modify": True, "draft": False, } repo = Repo(str(tmp_path / "work" / "bar")) assert repo.head.ref.name == "u/neophile" yaml = YAML() data = yaml.load(tmp_path / "work" / "bar" / ".pre-commit-config.yaml") assert data["repos"][2]["rev"] == "20.0.0" commit = repo.head.commit assert commit.author.name == "Someone" assert commit.author.email == "*****@*****.**" assert commit.message == f"{CommitMessage.title}\n\n{body}" nonlocal created_pr created_pr = True return CallbackResult(status=201) with aioresponses() as mock: register_mock_github_tags(mock, "ambv", "black", ["20.0.0", "19.10b0"]) mock.get("https://api.github.com/user", payload=user) mock.get( "https://api.github.com/repos/foo/bar", payload={"default_branch": "main"}, ) pattern = re.compile(r"https://api.github.com/repos/foo/bar/pulls\?.*") mock.get(pattern, payload=[]) mock.post( "https://api.github.com/repos/foo/bar/pulls", callback=check_pr_post, ) # Unfortunately, the mock_push fixture can't be used here because we # want to use git.Remote.push in create_upstream_git_repository. factory = Factory(config, session) processor = factory.create_processor() with patch_clone_from("foo", "bar", upstream_path): with patch.object(Remote, "push") as mock_push: mock_push.return_value = push_result await processor.process() assert mock_push.call_args_list == [ call("u/neophile:u/neophile", force=True) ] assert created_pr repo = Repo(str(tmp_path / "work" / "bar")) assert not repo.is_dirty() assert repo.head.ref.name == "master" assert "u/neophile" not in [h.name for h in repo.heads] assert "tmp-neophile" not in [r.name for r in repo.remotes]
async def test_allow_expressions(tmp_path: Path, session: ClientSession) -> None: tmp_repo = setup_kubernetes_repo(tmp_path / "tmp") upstream_path = tmp_path / "upstream" create_upstream_git_repository(tmp_repo, upstream_path) config = Configuration( allow_expressions=True, cache_enabled=False, repositories=[GitHubRepository(owner="foo", repo="bar")], work_area=tmp_path / "work", ) user = {"name": "Someone", "email": "*****@*****.**"} push_result = [PushInfo(PushInfo.NEW_HEAD, None, "", None)] created_pr = False def check_pr_post(url: str, **kwargs: Any) -> CallbackResult: assert json.loads(kwargs["data"]) == { "title": CommitMessage.title, "body": "- Update gafaelfawr Helm chart from 1.3.1 to v1.4.0\n", "head": "u/neophile", "base": "main", "maintainer_can_modify": True, "draft": False, } nonlocal created_pr created_pr = True return CallbackResult(status=201) with aioresponses() as mock: register_mock_helm_repository( mock, "https://kubernetes-charts.storage.googleapis.com/index.yaml", { "elasticsearch": ["1.26.2"], "kibana": ["3.0.1"] }, ) register_mock_helm_repository( mock, "https://kiwigrid.github.io/index.yaml", {"fluentd-elasticsearch": ["3.0.0"]}, ) register_mock_helm_repository( mock, "https://lsst-sqre.github.io/charts/index.yaml", {"gafaelfawr": ["1.3.1", "v1.4.0"]}, ) mock.get("https://api.github.com/user", payload=user) mock.get( "https://api.github.com/repos/foo/bar", payload={"default_branch": "main"}, ) pattern = re.compile(r"https://api.github.com/repos/foo/bar/pulls\?.*") mock.get(pattern, payload=[]) mock.post( "https://api.github.com/repos/foo/bar/pulls", callback=check_pr_post, ) # Unfortunately, the mock_push fixture can't be used here because we # want to use git.Remote.push in create_upstream_git_repository. factory = Factory(config, session) processor = factory.create_processor() with patch_clone_from("foo", "bar", upstream_path): with patch.object(Remote, "push") as mock_push: mock_push.return_value = push_result await processor.process() assert created_pr