diff --git a/Dockerfile b/Dockerfile index dc5df1b4..e661e216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,10 @@ ENTRYPOINT ["lando-cli"] CMD ["uwsgi"] ENV PYTHONUNBUFFERED=1 +# Set the MozBuild state path for `mach` autoformatting. +# Avoids any prompts in output during `mach bootstrap`. +ENV MOZBUILD_STATE_PATH /app/.mozbuild + # uWSGI configuration ENV UWSGI_MODULE=landoapi.wsgi:app \ UWSGI_SOCKET=:9000 \ diff --git a/hgext/postfix_hook.py b/hgext/postfix_hook.py deleted file mode 100644 index 6df5b4e7..00000000 --- a/hgext/postfix_hook.py +++ /dev/null @@ -1,24 +0,0 @@ -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -import binascii - -testedwith = b"5.5" - -# For clarity -PASS, FAIL = 0, 1 - - -def postfix_hook(ui, repo, replacements=None, wdirwritten=False, **kwargs): - """Hook that runs after `hg fix` is complete.""" - if wdirwritten: - ui.warn(b"Working directory was written; this should not happen\n") - return FAIL - - if replacements: - # Write a line containing the replacements after a separator - ui.write(b"\nREPLACEMENTS: ") - ui.write(b",".join(binascii.hexlify(binary) for binary in replacements)) - ui.write(b"\n") - - return PASS diff --git a/landoapi/hg.py b/landoapi/hg.py index 122614d9..c07d5440 100644 --- a/landoapi/hg.py +++ b/landoapi/hg.py @@ -2,16 +2,17 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import copy -from contextlib import contextmanager import configparser import logging import os -from pathlib import Path import shlex import shutil +import subprocess import tempfile import uuid +from contextlib import contextmanager +from pathlib import Path from typing import List, Optional import hglib @@ -101,21 +102,21 @@ class PatchConflict(PatchApplicationFailure): ) -def check_fix_output_for_replacements(fix_output: List[bytes]) -> Optional[List[str]]: - """Parses `hg fix` output. +class AutoformattingException(Exception): + """Exception when autoformatting fails to format a patch stack.""" + + pass - Returns: - A list of changeset hashes, or None if no changesets are changed. - """ - for line in fix_output: - if not line.startswith(b"REPLACEMENTS: "): - continue - replacements_list = line.split(b"REPLACEMENTS: ", maxsplit=1)[-1].split(b",") +AUTOFORMAT_COMMIT_MESSAGE = """ +No bug: apply code formatting via Lando - return [element.decode("latin-1") for element in replacements_list] +# ignore-this-changeset - return None +Output from `mach lint`: + +{output} +""".strip() class HgRepo: @@ -136,10 +137,6 @@ class HgRepo: "extensions.strip": "", "extensions.rebase": "", "extensions.set_landing_system": "/app/hgext/set_landing_system.py", - # Turn on fix extension for autoformatting, set to abort on failure - "extensions.fix": "", - "fix.failure": "abort", - "hooks.postfix": "python:/app/hgext/postfix_hook.py:postfix_hook", } def __init__(self, path, config=None): @@ -152,6 +149,13 @@ def __init__(self, path, config=None): if config: self.config.update(config) + @property + def mach_path(self) -> Optional[Path]: + """Return the `Path` to `mach`, if it exists.""" + mach_path = Path(self.path) / "mach" + if mach_path.exists(): + return mach_path + def _config_to_list(self): return ["{}={}".format(k, v) for k, v in self.config.items() if v is not None] @@ -280,7 +284,7 @@ def clean_repo(self, *, strip_non_public_commits=True): except hglib.error.CommandError: pass try: - self.run_hg(["purge", "--all"]) + self.run_hg(["purge"]) except hglib.error.CommandError: pass @@ -368,56 +372,166 @@ def apply_patch(self, patch_io_buf): + ["--logfile", f_msg.name] ) - def format(self) -> Optional[List[str]]: - """Run `hg fix` to format the currently checked-out stack, reading - fileset patterns for each formatter from the `.lando.ini` file in-tree.""" - # Avoid attempting to autoformat without `.lando.ini` in-tree. - lando_config_path = Path(self.path) / ".lando.ini" - if not lando_config_path.exists(): + def read_lando_config(self) -> Optional[configparser.ConfigParser]: + """Attempt to read the `.lando.ini` file.""" + try: + lando_ini_contents = self.read_checkout_file(".lando.ini") + except ValueError: return None # ConfigParser will use `:` as a delimeter unless told otherwise. # We set our keys as `formatter:pattern` so specify `=` as the delimiters. parser = configparser.ConfigParser(delimiters="=") - with lando_config_path.open() as f: - parser.read_file(f) + parser.read_string(lando_ini_contents) - # If the file doesn't have a `fix` section, exit early. - if not parser.has_section("fix"): - return None + return parser - fix_hg_command = [] - for key, value in parser.items("fix"): - if not key.endswith(":pattern"): - continue + def run_code_formatters(self) -> str: + """Run automated code formatters, returning the output of the process. - fix_hg_command += ["--config", f"fix.{key}={value}"] + Changes made by code formatters are applied to the working directory and + are not committed into version control. + """ + return self.run_mach_command(["lint", "--fix", "--outgoing"]) - # Exit if we didn't find any patterns. - if not fix_hg_command: - return None + def run_mach_bootstrap(self) -> str: + """Run `mach bootstrap` to configure the system for code formatting.""" + return self.run_mach_command( + [ + "bootstrap", + "--no-system-changes", + "--application-choice", + "browser", + ] + ) - # Run the formatters. - fix_hg_command += ["fix", "-r", "stack()"] - fix_output = self.run_hg(fix_hg_command).splitlines() + def run_mach_command(self, args: List[str]) -> str: + """Run a command using the local `mach`, raising if it is missing.""" + if not self.mach_path: + raise Exception("No `mach` found in local repo!") - # Update the working directory to the latest change. - self.run_hg(["update", "-C", "-r", "tip"]) + # Convert to `str` here so we can log the mach path. + command_args = [str(self.mach_path)] + args - # Exit if no revisions were reformatted. - pre_formatting_hashes = check_fix_output_for_replacements(fix_output) - if not pre_formatting_hashes: + try: + logger.info("running mach command", extra={"command": command_args}) + + output = subprocess.run( + command_args, + capture_output=True, + check=True, + cwd=self.path, + encoding="utf-8", + universal_newlines=True, + ) + + logger.info( + "output from mach command", + extra={ + "output": output.stdout, + }, + ) + + return output.stdout + + except subprocess.CalledProcessError as exc: + logger.exception( + "Failed to run mach command", + extra={ + "command": command_args, + "err": exc.stderr, + "output": exc.stdout, + }, + ) + + raise exc + + def format_stack_amend(self) -> Optional[List[str]]: + """Amend the top commit in the patch stack with changes from formatting.""" + try: + # Amend the current commit, using `--no-edit` to keep the existing commit message. + self.run_hg(["commit", "--amend", "--no-edit", "--landing_system", "lando"]) + + return [self.get_current_node().decode("utf-8")] + except hglib.error.CommandError as exc: + if exc.out.strip() == b"nothing changed": + # If nothing changed after formatting we can just return. + return + + raise exc + + def format_stack_tip(self, autoformat_output: str) -> Optional[List[str]]: + """Add an autoformat commit to the top of the patch stack. + + Return the commit hash of the autoformat commit as a `str`, + or return `None` if autoformatting made no changes. + """ + try: + # Create a new commit. + self.run_hg( + ["commit"] + + [ + "--message", + AUTOFORMAT_COMMIT_MESSAGE.format(output=autoformat_output), + ] + + ["--landing_system", "lando"] + ) + + return [self.get_current_node().decode("utf-8")] + + except hglib.error.CommandError as exc: + if exc.out.strip() == b"nothing changed": + # If nothing changed after formatting we can just return. + return + + raise exc + + def format_stack(self, stack_size: int) -> Optional[List[str]]: + """Format the patch stack for landing. + + Return a list of `str` commit hashes where autoformatting was applied, + or `None` if autoformatting was skipped. Raise `AutoformattingException` + if autoformatting failed for the current job. + """ + # Disable autoformatting if `.lando.ini` is missing or not enabled. + landoini_config = self.read_lando_config() + if ( + not landoini_config + or not landoini_config.has_section("autoformat") + or not landoini_config.getboolean("autoformat", "enabled") + ): return None - post_formatting_hashes = ( - self.run_hg(["log", "-r", "stack()", "-T", "{node}\n"]) - .decode("utf-8") - .splitlines()[len(pre_formatting_hashes) - 1 :] - ) + # If `mach` is not at the root of the repo, we can't autoformat. + if not self.mach_path: + logger.info("No `./mach` in the repo - skipping autoformat.") + return None - logger.info(f"revisions were reformatted: {', '.join(post_formatting_hashes)}") + try: + output = self.run_code_formatters() + except subprocess.CalledProcessError as exc: + logger.warning("Failed to run automated code formatters.") + logger.exception(exc) + + raise AutoformattingException( + "Failed to run automated code formatters." + ) from exc + + try: + # When the stack is just a single commit, amend changes into it. + if stack_size == 1: + return self.format_stack_amend() - return post_formatting_hashes + # If the stack is more than a single commit, create an autoformat commit. + return self.format_stack_tip(output) + + except HgException as exc: + logger.warning("Failed to create an autoformat commit.") + logger.exception(exc) + + raise AutoformattingException( + "Failed to apply code formatting changes to the repo." + ) from exc def push(self, target, bookmark=None): if not os.getenv(REQUEST_USER_ENV_VAR): @@ -472,6 +586,10 @@ def get_remote_head(self, source: str) -> bytes: assert len(cset) == 12, cset return cset + def get_current_node(self) -> bytes: + """Return the currently checked out node.""" + return self.run_hg(["identify", "-r", ".", "-i"]) + def update_from_upstream(self, source, remote_rev): # Pull and update to remote tip. cmds = [ diff --git a/landoapi/landing_worker.py b/landoapi/landing_worker.py index 327ac8b2..d3057f1b 100644 --- a/landoapi/landing_worker.py +++ b/landoapi/landing_worker.py @@ -12,12 +12,12 @@ import subprocess import time -import hglib import kombu from flask import current_app from landoapi import patches from landoapi.hg import ( + AutoformattingException, HgRepo, LostPushRace, NoDiffStartLine, @@ -451,16 +451,16 @@ def run_job( self.notify_user_of_landing_failure(job) return True - # Run `hg fix` configured formatters if enabled + # Run automated code formatters if enabled. if repo.autoformat_enabled: try: - replacements = hgrepo.format() + replacements = hgrepo.format_stack(len(patch_bufs)) - # If autoformatting changed any changesets, note those in the job. + # If autoformatting added any changesets, note those in the job. if replacements: job.formatted_replacements = replacements - except hglib.error.CommandError as exc: + except AutoformattingException as exc: message = ( "Lando failed to format your patch for conformity with our " "formatting policy. Please see the details below.\n\n" diff --git a/landoapi/repos.py b/landoapi/repos.py index 8ca0e231..704a8bc9 100644 --- a/landoapi/repos.py +++ b/landoapi/repos.py @@ -72,6 +72,7 @@ class Repo: short_name: str = "" legacy_transplant: bool = False approval_required: bool = False + autoformat_enabled: bool = False commit_flags: list[tuple[str, str]] = field(default_factory=list) config_override: dict = field(default_factory=dict) product_details_url: str = "" @@ -91,15 +92,6 @@ def __post_init__(self): if not self.short_name: self.short_name = self.tree - @property - def autoformat_enabled(self) -> bool: - """Return `True` if formatting is enabled for the repo.""" - if not self.config_override: - # Empty config override always indicates no autoformat. - return False - - return any(config.startswith("fix") for config in self.config_override.keys()) - @property def phab_identifier(self) -> str: """Return a valid Phabricator identifier as a `str`.""" @@ -193,7 +185,7 @@ def phab_identifier(self) -> str: access_group=SCM_LEVEL_1, push_path="ssh://autoland.hg//repos/third-repo", pull_path="http://hg.test/third-repo", - config_override={"fix.black:command": "black -- -"}, + autoformat_enabled=True, approval_required=True, ), # Approval is required for the uplift dev repo @@ -226,7 +218,6 @@ def phab_identifier(self) -> str: url="https://hg.mozilla.org/conduit-testing/vct", access_group=SCM_CONDUIT, push_bookmark="@", - config_override={"fix.black:command": "black -- -"}, ), }, "devsvcstage": { @@ -274,9 +265,9 @@ def phab_identifier(self) -> str: access_group=SCM_LEVEL_3, short_name="mozilla-central", commit_flags=[DONTBUILD], - config_override={"fix.black:command": "black -- -"}, product_details_url="https://product-details.mozilla.org" "/1.0/firefox_versions.json", + autoformat_enabled=True, ), "comm-central": Repo( tree="comm-central", @@ -377,6 +368,10 @@ def ready(self): logger.info("Cloning repo.", extra={"repo": name}) r.clone(repo.pull_path) + # Ensure packages required for automated code formatting are installed. + if repo.autoformat_enabled: + r.run_mach_bootstrap() + logger.info("Repo ready.", extra={"repo": name}) self.repo_paths[name] = path diff --git a/tests/test_landings.py b/tests/test_landings.py index d793fcc3..6b1df8a9 100644 --- a/tests/test_landings.py +++ b/tests/test_landings.py @@ -2,12 +2,14 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import io + import pytest import textwrap import unittest.mock as mock from landoapi import patches -from landoapi.hg import HgRepo +from landoapi.hg import AUTOFORMAT_COMMIT_MESSAGE, HgRepo from landoapi.landing_worker import LandingWorker from landoapi.models.landing_job import LandingJob, LandingJobStatus from landoapi.models.transplant import Transplant, TransplantStatus @@ -166,6 +168,27 @@ def _create_transplant( +adding one more line """.strip() +PATCH_NORMAL_3 = r""" +# HG changeset patch +# User Test User +# Date 0 0 +# Thu Jan 01 00:00:00 1970 +0000 +# Diff Start Line 7 +add another file. +diff --git a/test.txt b/test.txt +deleted file mode 100644 +--- a/test.txt ++++ /dev/null +@@ -1,1 +0,0 @@ +-TEST +diff --git a/blah.txt b/blah.txt +new file mode 100644 +--- /dev/null ++++ b/blah.txt +@@ -0,0 +1,1 @@ ++TEST +""".strip() + PATCH_PUSH_LOSER = r""" # HG changeset patch # User Test User @@ -182,7 +205,7 @@ def _create_transplant( +adding one more line again """.strip() -PATCH_FORMATTING_PATTERN = r""" +PATCH_FORMATTING_PATTERN_PASS = r""" # HG changeset patch # User Test User # Date 0 0 @@ -191,12 +214,82 @@ def _create_transplant( add formatting config diff --git a/.lando.ini b/.lando.ini +new file mode 100644 --- /dev/null +++ b/.lando.ini @@ -0,0 +1,3 @@ -+[fix] -+fakefmt:pattern = set:**.txt -+fail:pattern = set:**.txt ++[autoformat] ++enabled = True ++ +diff --git a/mach b/mach +new file mode 100755 +--- /dev/null ++++ b/mach +@@ -0,0 +1,30 @@ ++#!/usr/bin/env python3 ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++# Fake formatter that rewrites text to mOcKiNg cAse ++ ++import pathlib ++import sys ++ ++HERE = pathlib.Path(__file__).resolve().parent ++ ++def split_chars(string) -> list: ++ return [char for char in string] ++ ++ ++if __name__ == "__main__": ++ testtxt = HERE / "test.txt" ++ if not testtxt.exists(): ++ sys.exit(0) ++ with testtxt.open() as f: ++ stdin_content = f.read() ++ stdout_content = [] ++ ++ for i, word in enumerate(split_chars(stdin_content)): ++ stdout_content.append(word.upper() if i % 2 == 0 else word.lower()) ++ ++ with testtxt.open("w") as f: ++ f.write("".join(stdout_content)) ++ sys.exit(0) + +""".strip() + +PATCH_FORMATTING_PATTERN_FAIL = r""" +# HG changeset patch +# User Test User +# Date 0 0 +# Thu Jan 01 00:00:00 1970 +0000 +# Diff Start Line 7 +add formatting config + +diff --git a/.lando.ini b/.lando.ini +new file mode 100644 +--- /dev/null ++++ b/.lando.ini +@@ -0,0 +1,3 @@ ++[autoformat] ++enabled = True ++ +diff --git a/mach b/mach +new file mode 100755 +--- /dev/null ++++ b/mach +@@ -0,0 +1,9 @@ ++#!/usr/bin/env python3 ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++# Fake formatter that fails to run. ++import sys ++sys.exit("MACH FAILED") ++ + """.strip() PATCH_FORMATTED_1 = r""" @@ -446,28 +539,28 @@ def test_format_patch_success_unchanged( upload_patch, ): """Tests automated formatting happy path where formatters made no changes.""" + tree = "mozilla-central" treestatus = treestatusdouble.get_treestatus_client() - treestatusdouble.open_tree("mozilla-central") + treestatusdouble.open_tree(tree) repo = Repo( - tree="mozilla-central", + tree=tree, url=hg_server, push_path=hg_server, pull_path=hg_server, access_group=SCM_LEVEL_3, - config_override={"fix.fakefmt:command": "cat"}, + autoformat_enabled=True, ) hgrepo = HgRepo(hg_clone.strpath, config=repo.config_override) - upload_patch(1, patch=PATCH_FORMATTING_PATTERN) - upload_patch(2, patch=PATCH_FORMATTED_1) - upload_patch(3, patch=PATCH_FORMATTED_2) + upload_patch(1, patch=PATCH_FORMATTING_PATTERN_PASS) + upload_patch(2, patch=PATCH_NORMAL_3) job = LandingJob( status=LandingJobStatus.IN_PROGRESS, requester_email="test@example.com", - repository_name="mozilla-central", - revision_to_diff_id={"1": 1, "2": 2, "3": 3}, - revision_order=["1", "2", "3"], + repository_name=tree, + revision_to_diff_id={"1": 1, "2": 2}, + revision_order=["1", "2"], attempts=1, ) @@ -484,13 +577,15 @@ def test_format_patch_success_unchanged( assert ( job.status == LandingJobStatus.LANDED ), "Successful landing should set `LANDED` status." - assert job.formatted_replacements is None assert ( mock_trigger_update.call_count == 1 ), "Successful landing should trigger Phab repo update." + assert ( + job.formatted_replacements is None + ), "Autoformat making no changes should leave `formatted_replacements` empty." -def test_format_patch_success_changed( +def test_format_single_success_changed( app, db, s3, @@ -501,32 +596,118 @@ def test_format_patch_success_changed( monkeypatch, upload_patch, ): - """Tests automated formatting happy path where formatters made - changes before landing. - """ + """Test formatting a single commit via amending.""" + tree = "mozilla-central" treestatus = treestatusdouble.get_treestatus_client() - treestatusdouble.open_tree("mozilla-central") + treestatusdouble.open_tree(tree) repo = Repo( - tree="mozilla-central", + tree=tree, url=hg_server, push_path=hg_server, pull_path=hg_server, access_group=SCM_LEVEL_3, - config_override={ - "fix.fakefmt:command": "python /app/tests/fake_formatter.py", - "fix.fakefmt:linerange": "--lines={first}:{last}", - }, + autoformat_enabled=True, ) + # Push the `mach` formatting patch. hgrepo = HgRepo(hg_clone.strpath, config=repo.config_override) + with hgrepo.for_push("test@example.com"): + hgrepo.apply_patch(io.BytesIO(PATCH_FORMATTING_PATTERN_PASS.encode("utf-8"))) + hgrepo.push(repo.push_path) + pre_landing_tip = hgrepo.run_hg(["log", "-r", "tip", "-T", "{node}"]).decode( + "utf-8" + ) - upload_patch(1, patch=PATCH_FORMATTING_PATTERN) + # Upload a patch for formatting. + upload_patch(2, patch=PATCH_FORMATTED_1) + job = LandingJob( + status=LandingJobStatus.IN_PROGRESS, + requester_email="test@example.com", + repository_name=tree, + revision_to_diff_id={"2": 2}, + revision_order=["2"], + attempts=1, + ) + + worker = LandingWorker(sleep_seconds=0.01) + + # Mock `phab_trigger_repo_update` so we can make sure that it was called. + mock_trigger_update = mock.MagicMock() + monkeypatch.setattr( + "landoapi.landing_worker.LandingWorker.phab_trigger_repo_update", + mock_trigger_update, + ) + + assert worker.run_job( + job, repo, hgrepo, treestatus, "landoapi.test.bucket" + ), "`run_job` should return `True` on a successful run." + assert ( + job.status == LandingJobStatus.LANDED + ), "Successful landing should set `LANDED` status." + assert ( + mock_trigger_update.call_count == 1 + ), "Successful landing should trigger Phab repo update." + + with hgrepo.for_push(job.requester_email): + repo_root = hgrepo.run_hg(["root"]).decode("utf-8").strip() + + # Get the commit message. + desc = hgrepo.run_hg(["log", "-r", "tip", "-T", "{desc}"]).decode("utf-8") + + # Get the content of the file after autoformatting. + tip_content = hgrepo.run_hg( + ["cat", "--cwd", repo_root, "-r", "tip", "test.txt"] + ) + + # Get the hash behind the tip commit. + hash_behind_current_tip = hgrepo.run_hg( + ["log", "-r", "tip^", "-T", "{node}"] + ).decode("utf-8") + + assert tip_content == TESTTXT_FORMATTED_1, "`test.txt` is incorrect in base commit." + + assert ( + desc == "add another file for formatting 1" + ), "Autoformat via amend should not change commit message." + + assert ( + hash_behind_current_tip == pre_landing_tip + ), "Autoformat via amending should only land a single commit." + + +def test_format_stack_success_changed( + app, + db, + s3, + mock_repo_config, + hg_server, + hg_clone, + treestatusdouble, + monkeypatch, + upload_patch, +): + """Test formatting a stack via an autoformat tip commit.""" + tree = "mozilla-central" + treestatus = treestatusdouble.get_treestatus_client() + treestatusdouble.open_tree(tree) + repo = Repo( + tree=tree, + url=hg_server, + push_path=hg_server, + pull_path=hg_server, + access_group=SCM_LEVEL_3, + autoformat_enabled=True, + ) + + hgrepo = HgRepo(hg_clone.strpath, config=repo.config_override) + + upload_patch(1, patch=PATCH_FORMATTING_PATTERN_PASS) upload_patch(2, patch=PATCH_FORMATTED_1) upload_patch(3, patch=PATCH_FORMATTED_2) job = LandingJob( status=LandingJobStatus.IN_PROGRESS, requester_email="test@example.com", - repository_name="mozilla-central", + repository_name=tree, revision_to_diff_id={"1": 1, "2": 2, "3": 3}, revision_order=["1", "2", "3"], attempts=1, @@ -541,53 +722,38 @@ def test_format_patch_success_changed( mock_trigger_update, ) - # The landed commit hashes affected by autoformat - formatted_replacements = [ - "12be32a8a3ff283e0836b82be959fbd024cf271b", - "15b05c609cf43b49e7360eaea4de938158d18c6a", - ] - assert worker.run_job( job, repo, hgrepo, treestatus, "landoapi.test.bucket" ), "`run_job` should return `True` on a successful run." assert ( job.status == LandingJobStatus.LANDED ), "Successful landing should set `LANDED` status." - assert ( - job.formatted_replacements == formatted_replacements - ), "Did not correctly save hashes of formatted revisions" assert ( mock_trigger_update.call_count == 1 ), "Successful landing should trigger Phab repo update." with hgrepo.for_push(job.requester_email): - # Get repo root since `-R` does not change relative directory, so - # we would need to pass the absolute path to `test.txt` repo_root = hgrepo.run_hg(["root"]).decode("utf-8").strip() - # Get the content of `test.txt` - rev2_content = hgrepo.run_hg( - ["cat", "--cwd", repo_root, "-r", "tip^", "test.txt"] - ) + # Get the commit message. + desc = hgrepo.run_hg(["log", "-r", "tip", "-T", "{desc}"]).decode("utf-8") + + # Get the content of the file after autoformatting. rev3_content = hgrepo.run_hg( ["cat", "--cwd", repo_root, "-r", "tip", "test.txt"] ) - # Get the commit hashes - nodes = ( - hgrepo.run_hg(["log", "-r", "tip^::tip", "-T", "{node}\n"]) - .decode("utf-8") - .splitlines() - ) - assert ( - rev2_content == TESTTXT_FORMATTED_1 + rev3_content == TESTTXT_FORMATTED_2 ), "`test.txt` is incorrect in base commit." - assert rev3_content == TESTTXT_FORMATTED_2, "`test.txt` is incorrect in tip commit." - assert all( - replacement in nodes for replacement in job.formatted_replacements - ), "Values in `formatted_replacements` field should be in the landed hashes." + assert ( + "# ignore-this-changeset" in desc + ), "Commit message for autoformat commit should contain `# ignore-this-changeset`." + + assert ( + desc == AUTOFORMAT_COMMIT_MESSAGE.format(output="").strip() + ), "Autoformat commit has incorrect commit message." def test_format_patch_fail( @@ -602,29 +768,27 @@ def test_format_patch_fail( upload_patch, ): """Tests automated formatting failures before landing.""" + tree = "mozilla-central" treestatus = treestatusdouble.get_treestatus_client() - treestatusdouble.open_tree("mozilla-central") + treestatusdouble.open_tree(tree) repo = Repo( - tree="mozilla-central", + tree=tree, access_group=SCM_LEVEL_3, url=hg_server, push_path=hg_server, pull_path=hg_server, - config_override={ - # Force failure by setting a formatter that returns exit code 1 - "fix.fail:command": "exit 1" - }, + autoformat_enabled=True, ) hgrepo = HgRepo(hg_clone.strpath, config=repo.config_override) - upload_patch(1, patch=PATCH_FORMATTING_PATTERN) + upload_patch(1, patch=PATCH_FORMATTING_PATTERN_FAIL) upload_patch(2) upload_patch(3) job = LandingJob( status=LandingJobStatus.IN_PROGRESS, requester_email="test@example.com", - repository_name="mozilla-central", + repository_name=tree, revision_to_diff_id={"1": 1, "2": 2, "3": 3}, revision_order=["1", "2", "3"], attempts=1, @@ -644,6 +808,9 @@ def test_format_patch_fail( assert ( job.status == LandingJobStatus.FAILED ), "Failed autoformatting should set `FAILED` job status." + assert ( + "Lando failed to format your patch" in job.error + ), "Error message is not set to show autoformat caused landing failure." assert ( mock_notify.call_count == 1 ), "User should be notified their landing was unsuccessful due to autoformat." @@ -669,10 +836,7 @@ def test_format_patch_no_landoini( url=hg_server, push_path=hg_server, pull_path=hg_server, - config_override={ - # If the `.lando.ini` file existed, this formatter would run and fail - "fix.fail:command": "exit 1" - }, + autoformat_enabled=True, ) hgrepo = HgRepo(hg_clone.strpath, config=repo.config_override)