Hi guys,

some of you were on the Monday's meeting, where discussion raised a request for automatic pull request checker and next generator. I took a look on it and implemented very basic version just to see, whether it's worth it. Please note that some parts are hardcoded to my setup.

Currently it only:
1) checks all open pull requests for changes from previous run
2) runs check_patch and boot test on each changed pull request. (changed commits, not comments) 3) if there are pull requests, which were not used in the previous next-branch or were used and shouldn't be used now, next-branch is freshly recreated. If it passes the boot test, can be pushed to the original repo (currently commented out). 4) update the github pull request comments (currently only prints the informations to the cmdline but should be simple to push these messages to each pull request.
5) stores the information to github_checker.sav for the next run


Output of the first run:
--< UPDATE >--
Checking changes of PR1438
Checking PR1438...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1438
  git checkout master --force
  git branch -D github-1438
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
PASS
Checking changes of PR1437
Checking PR1437...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1437
  git checkout master --force
  git branch -D github-1437
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
PASS
Checking changes of PR1436
Checking PR1436...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1436
  git checkout master --force
  git branch -D github-1436
FAIL (checkpatch)
  git am --abort
Checking changes of PR1435
Checking PR1435...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1435
  git checkout master --force
  git branch -D github-1435
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
PASS
Checking changes of PR1434
Checking PR1434...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1434
  git checkout master --force
  git branch -D github-1434
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
PASS
Recreating next
  git fetch upstream
  git checkout next
  git reset --hard upstream/master
Merging PR1434...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1434.patch -O /var/tmp/tmpL2zCmD
  git am -3 /var/tmp/tmpL2zCmD
PASS
Merging PR1435...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1435.patch -O /var/tmp/tmpe2388C
  git am -3 /var/tmp/tmpe2388C
PASS
Merging PR1437...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1437.patch -O /var/tmp/tmpFKVbkQ
  git am -3 /var/tmp/tmpFKVbkQ
PASS
Merging PR1438...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1438.patch -O /var/tmp/tmpSkQBs4
  git am -3 /var/tmp/tmpSkQBs4
PASS
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
Next is ready
  git checkout master
--< MESSAGES >--
PR1434
  check_patch :+1:
  boot_test :+1:
PR1435
  check_patch :+1:
  boot_test :+1:
PR1436
  check_patch :-1:
PR1437
  check_patch :+1:
  boot_test :+1:
PR1438
  check_patch :+1:
  boot_test :+1:

Output of the second run:
--< UPDATE >--
Checking changes of PR1438
Checking changes of PR1437
Checking changes of PR1436
Checking changes of PR1435
Checking changes of PR1434
--< MESSAGES >--


As you can see there were no changes, no new messages in pull request and no next recreation in the second run. Now output of another run, when two new pull requests are added:
--< UPDATE >--
Checking changes of PR1438
Checking changes of PR1437
Checking changes of PR1436
Checking changes of PR1435
Checking changes of PR1434
Checking changes of PR1433
Checking PR1433...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1433
  git checkout master --force
  git branch -D github-1433
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
PASS
Checking changes of PR1431
Checking PR1431...
  AUTOTEST_PATH=../../../ tools/check_patch.py -g 1431
  git checkout master --force
  git branch -D github-1431
FAIL (checkpatch)
  git am --abort
Recreating next
  git fetch upstream
  git checkout next
  git reset --hard upstream/master
Merging PR1433...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1433.patch -O /var/tmp/tmpTixM1R
  git am -3 /var/tmp/tmpTixM1R
PASS
Merging PR1434...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1434.patch -O /var/tmp/tmpDzglAn
  git am -3 /var/tmp/tmpDzglAn
PASS
Merging PR1435...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1435.patch -O /var/tmp/tmpESs4ja
  git am -3 /var/tmp/tmpESs4ja
PASS
Merging PR1437...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1437.patch -O /var/tmp/tmps6Uki9
  git am -3 /var/tmp/tmps6Uki9
PASS
Merging PR1438...
  git rev-parse HEAD
wget https://github.com/autotest/virt-test/pull/1438.patch -O /var/tmp/tmp7tlW4Q
  git am -3 /var/tmp/tmp7tlW4Q
PASS
AUTOTEST_PATH=../../../ ./run -t qemu --tests='boot' --no-downloads -k --keep-image-between-tests
Next is ready
  git checkout master
--< MESSAGES >--
PR1431
  check_patch :-1:
PR1433
  check_patch :+1:
  boot_test :+1:

...
there is one additional message not shown here:
  Potential conflict with other pull request, not applying to next. :-1:

which means that patch which is applicable on master has conflicts when applying on next (some previous pull requests collides with it)


As you can see it found them successfully, checked only the new ones, found that the 1431 fails to apply on current master so ignored it while recreating the next.


Honestly I don't see very big income. Users should run check_patch themselves to see, whether their pull request is ready. Similarly no maintainer should apply test unless he tests, that "boot" test is not screwed up. Anyway, consider this as a discussion starter, maybe I just didn't get the point correctly.

Kind regards,
Lukáš
#!/usr/bin/env python

import sys
import os
import getpass
import datetime
import github
import tempfile
import subprocess
from astroid.node_classes import Exec
import pickle


def execute(cmd, ignore_status=False):
    """
    Executes command using subprocess
    :return: tuple($status, $output)
    """
    print "  " + cmd
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE, shell=True)
    out = process.communicate()
    ret = process.poll()
    if ret and not ignore_status:
        raise OSError("Fail to execute %s (%s)\nSTDOUT:\n%s\nSTDERR:\n%s"
                      % (cmd, ret, out[0], out[1]))
    return (ret, "\n".join(out))


def test_boot():
    """
    Test virt-test using 'boot' test
    :return: True on error
    """
    ret, out = execute("AUTOTEST_PATH=../../../ "
                       "./run -t qemu --tests='boot' --no-downloads -k "
                       "--keep-image-between-tests", False)
    if not ret and 'boot: PASS' in out:
        return False    # Correctly executed
    else:
        # Remove the image and try it again
        execute("/var/lib/virt_test/images/jeos-19-64.qcow2", False)
        ret, out = execute("AUTOTEST_PATH=../../../ "
                           "./run -t qemu --tests='boot'", False)
        if not ret and 'boot: PASS' in out:
            return False
        return True


class GITTree(list):
    @staticmethod
    def _merge_pull_request(pr):
        """
        Try to merge pull request
        """
        print "Merging PR%s..." % pr.pr.number
        old_head = execute("git rev-parse HEAD")[1].strip()
        patch_file = tempfile.NamedTemporaryFile('w', dir="/var/tmp/",
                                                 delete=True)
        execute("wget %s -O %s" % (pr.pr.patch_url, patch_file.name))
        ret = execute("git am -3 %s" % patch_file.name, True)[0]
        patch_file.close()
        if ret:
            execute("git am --abort", True)
            execute("git reset --hard %s" % old_head)
            pr.set_valid(False)
            pr.add_message("Potential conflict with other pull "
                           "request, not applying to next. :-1:\n")
            print "FAIL"
            return
        print "PASS"

    def recreate(self, prs):
        """
        Recreate next using prs pull requests
        """
        print "Recreating next"
        execute("git fetch origin")
        execute("git checkout next")
        execute("git reset --hard origin/master")
        for pr in prs:
            if pr.is_valid():
                self._merge_pull_request(pr)

        if test_boot():
            print "Next is not executable"
            execute("git checkout master")
            return

        print "Next is ready"
        # TODO: Uncomment this if you want next to be automatically updated
        # execute("git push origin next --force")
        execute("git checkout master")


class PullRequest(object):
    """
    Pull request with additional data
    """
    def __init__(self, pull_request):
        self.__commits = []
        self.__valid = True
        self.__message = ""
        self.pr = pull_request

    def is_valid(self):
        return self.__valid

    def set_valid(self, status):
        self.__valid = status

    def check(self):
        """
        Check this pull request
        """
        i = 0
        old_len = len(self.__commits)
        for commit in self.pr.get_commits():
            if i >= old_len or commit.sha != self.__commits[i]:
                return self._run_check_patch()
            i += 1
        if i != old_len:
            return self._run_check_patch()

    def _run_check_patch(self):
        """
        Runs checkpatch
        :return: True when __valid status changes
        """
        print "Checking PR%s..." % self.pr.number
        ret = execute("AUTOTEST_PATH=../../../ tools/check_patch.py -g %s"
                      % self.pr.number, True)[0]
        # Some pull request might add files, which might be removed by checkout
        execute("git checkout master --force", True)
        execute("git branch -D github-%s" % self.pr.number, True)
        self.__commits = [commit.sha for commit in self.pr.get_commits()]
        if ret:     # Checkpatch failed
            print "FAIL (checkpatch)"
            execute("git am --abort", True)
            self.add_message("check_patch :-1:")
            if self.is_valid:
                self.set_valid(False)
                return True
            else:
                return False

        # Checkpatch passed, run additional tests
        self.add_message("check_patch :+1:")
        # Put additional tests here
        if test_boot():
            self.add_message("boot_test :-1:")
            if self.is_valid():
                self.set_valid(False)
                print "FAIL (boot)"
                return True
        else:
            self.add_message("boot_test :+1:")

        print "PASS"
        if not self.is_valid():
            self.set_valid(True)
            return True
        return False

    def sync_message(self):
        """
        Sends messages as pull request comments
        """
        if self.__message:
            # TODO: Send to github
            print "PR%s\n  %s" % (self.pr.number,
                                self.__message[:-1].replace("\n", "\n  "))
            self.__message = ""

    def add_message(self, msg):
        """
        Add message
        """
        self.__message += "%s\n" % msg


class PRCache(object):
    def __init__(self):
        self.__cache = {}
        self.__numbers = set()
        self.__idx = 0

    def __iter__(self):
        for number in self.__numbers:
            yield self.__cache[number]

    def update(self, prs):
        """
        Check for changes, add new ones, removes old ones.
        :param prs: pull requests
        :return: True if new or updated acceptable pull request is present
        """
        changed = False
        _prs = set()
        for pr in prs:
            if self.check(pr):
                changed = True
            _prs.add(pr.number)
        for pr in self.__numbers.difference(_prs):
            if self.remove(pr):
                changed = True
        return changed

    def check(self, pr):
        """
        Check pull request for changes
        :return: True when pull request which is ready to next changes
        """
        print "Checking changes of PR%s" % pr.number
        if pr.number not in self.__numbers:
            self.add(pr)
        return self.__cache[pr.number].check()

    def add(self, pr):
        """
        Adds pull request to this cache
        """
        self.__cache[pr.number] = PullRequest(pr)
        self.__numbers.add(pr.number)

    def remove(self, number):
        """
        Removes pull request from cache
        """
        if number not in self.__cache:
            raise IndexError(number)
        changed = False
        if self.__cache[number].is_valid():
            changed = True
        del(self.__cache[number])
        self.__numbers.remove(number)
        return changed


class GHChecker(object):
    def __init__(self, gh, repo):
        self.__gh = gh
        self.__repo = gh.get_repo(repo)
        # Cache of open pull request and their status
        self.__pr_cache = PRCache()
        # Branch with all good open pull request applied
        self.__next_tree = GITTree()

    def update(self):
        print "--< UPDATE >--"
        if self.__pr_cache.update(self.__repo.get_pulls(state="open")):
            self.__next_tree.recreate(self.__pr_cache)

        print "--< MESSAGES >--"
        for pr in self.__pr_cache:
            pr.sync_message()


if __name__ == "__main__":
    try:
        checker = pickle.load(open('github_checker.sav', 'rb'))
    except Exception:
        print "Previous state load failed"
        #gh = github.Github()
        gh = github.Github(login_or_token=raw_input("Enter github username: "),
                           password=getpass.getpass('Enter github password: '))
        repo = 'autotest/virt-test'

        checker = GHChecker(gh, repo)
    checker.update()
    pickle.dump(checker, open('github_checker.sav', 'wb'))
_______________________________________________
Virt-test-devel mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/virt-test-devel

Reply via email to