blob: 8374affeb58b8aff5fa5cfba61cd94f3d9c2fc27 [file] [log] [blame]
"""Command line interface tests for llvmprojs.py
Note that although this uses the unittest framework, it does *not* contain unit
tests.
"""
import shutil
import os
import subprocess
import unittest
from tempfile import mkdtemp
from uuid import uuid4
from linaropy.cd import cd
# TODO: move this somewhere more public (maybe linaropy?)
def debug(test):
"""
Decorator that dumps the output of any subprocess.CalledProcessError
exception. Use this to decorate a test function when you can't tell what the
problem is.
"""
def wrapper(*args, **kwargs):
# Catch any exceptions so we can dump all the output
try:
test(*args, **kwargs)
except subprocess.CalledProcessError as exc:
print("Error in {}:".format(test.__name__))
print("Command {} exited with error code {}:\n{}".format(
exc.cmd, exc.returncode, exc.output))
return wrapper
class Testllvmprojs(unittest.TestCase):
python = "python3"
script = os.path.join("scripts", "llvm.py")
@classmethod
def __create_dummy_commit(cls):
filename = "filethatshouldntexist"
cls.run_quietly(["touch", filename])
cls.run_quietly(["git", "add", filename])
cls.run_quietly(["git", "commit", "-m", "Dummy commit"])
@classmethod
def __create_dummy_repo(cls, repopath):
if not os.path.isdir(repopath):
os.makedirs(repopath)
with cd(repopath):
cls.run_quietly(["git", "init"])
cls.__create_dummy_commit()
@classmethod
def __add_worktree(cls, repopath, worktreepath, branch):
with cd(repopath):
cls.run_quietly(["git", "worktree", "add", worktreepath,
"-b", branch])
@classmethod
def __get_subproj_repo(cls, subproj):
return os.path.join(cls.repos, subproj)
@staticmethod
def run_with_output(*args, **kwargs):
"""Helper for running a command and capturing stdout and stderr"""
kwargs["stderr"] = subprocess.STDOUT
return str(subprocess.check_output(*args, **kwargs), 'utf-8')
@staticmethod
def run_quietly(*args, **kwargs):
"""
Helper for running a command and ignoring stdout and stderr. Exceptions
are still thrown if something goes wrong
"""
kwargs["stdout"] = subprocess.DEVNULL
kwargs["stderr"] = subprocess.DEVNULL
return subprocess.check_call(*args, **kwargs)
@classmethod
def setUpClass(cls):
"""Create the file structure and environment that llvmprojs expects"""
cls.llvm_root = mkdtemp()
cls.repos = os.path.join(cls.llvm_root, "repos")
cls.all_repos = ("llvm", "clang", "compiler-rt", "lld", "lldb",
"libcxx", "libcxxabi", "libunwind", "test-suite")
# Create dummy repos
for reponame in cls.all_repos:
cls.__create_dummy_repo(cls.__get_subproj_repo(reponame))
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.llvm_root)
@classmethod
def setUp(cls):
cls.env = "src" + str(uuid4())
cls.llvm_src = os.path.join(cls.llvm_root, cls.env, "llvm")
# Create LLVM worktree
cls.branch = "br" + str(uuid4())
cls.__add_worktree(cls.__get_subproj_repo("llvm"), cls.llvm_src,
cls.branch)
# Set up the environment variables
os.environ["LLVM_ROOT"] = cls.llvm_root
@classmethod
def tearDown(cls):
# Clean up the directories where we might have added subprojects.
# This isn't 100% clean, because we don't clean up the repos between
# tests (so any branches will remain), but it's good enough for the
# current tests.
for subprojdir in (os.path.join(cls.llvm_src, "projects"),
os.path.join(cls.llvm_src, "tools")):
if os.path.isdir(subprojdir):
shutil.rmtree(subprojdir)
os.makedirs(subprojdir)
# Run prune on the original repos, to remove any dangling worktrees.
for reponame in cls.all_repos:
repopath = cls.__get_subproj_repo(reponame)
with cd(repopath):
cls.run_quietly(["git", "worktree", "prune"])
def test_dump_empty_config(self):
"""
Test that we're correctly dumping an empty configuration (i.e. no
projects linked) when running llvmprojs without arguments.
"""
output = self.run_with_output(
[self.python, self.script, self.env, "projects"])
self.assertRegex(output, "Projects linked:.*\n.*none.*")
def test_add_remove_subprojects(self):
"""
Test that we can add and remove one or several subprojects.
"""
output = self.run_with_output(
[self.python, self.script, self.env, "projects", "--add", "clang"])
self.assertRegex(output, "Projects linked:.*\n.*clang.*")
output = self.run_with_output([self.python, self.script, self.env, "projects",
"--add", "libcxx", "lldb"])
self.assertRegex(
output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*libcxx.*\n" +
".*lldb.*")
output = self.run_with_output(
[self.python, self.script, self.env, "projects", "--remove", "libcxx"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*lldb.*")
output = self.run_with_output([self.python, self.script, self.env, "projects",
"--remove", "clang", "lldb"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*none.*")
def test_add_remove_invalid_subprojects(self):
"""
Test that we error out nicely when trying to add/remove invalid
subprojects.
"""
with self.assertRaises(subprocess.CalledProcessError) as context:
self.run_with_output([self.python, self.script, self.env, "projects",
"--add", "inventedsubproject"])
self.assertRegex(
str(context.exception.output),
"(.*\n)*.*invalid choice:.*inventedsubproject(.*\n)*")
with self.assertRaises(subprocess.CalledProcessError) as context:
self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--remove",
"inventedsubproject"])
self.assertRegex(
str(context.exception.output),
"(.*\n)*.*invalid choice:.*inventedsubproject(.*\n)*")
def test_duplicate_add_remove(self):
"""
Test that we don't crash when trying to add / remove the same subproject
twice with the same command.
"""
output = self.run_with_output([self.python, self.script, self.env, "projects",
"--add", "clang", "lld", "clang"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*lld.*")
output = self.run_with_output([self.python, self.script, self.env, "projects",
"--remove", "lld", "lld", "clang"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*none.*")
def test_redundant_add_remove(self):
"""
Test that we can add a subproject that already exists (either because it
was added by our script or manually) or remove a subproject that doesn't
exist.
"""
self.__add_worktree(self.__get_subproj_repo("clang"),
os.path.join(self.llvm_src, "tools", "clang"),
self.branch)
self.__add_worktree(
self.__get_subproj_repo("compiler-rt"),
os.path.join(self.llvm_src, "projects", "compiler-rt"),
self.branch)
output = self.run_with_output(
[self.python, self.script, self.env, "projects", "--add", "clang"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*")
output = self.run_with_output([self.python, self.script, self.env, "projects",
"--add", "compiler-rt", "lld"])
self.assertRegex(
output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*compiler-rt.*\n" +
".*lld.*")
output = self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--remove",
"lldb",
"libcxx",
"lld"])
self.assertRegex(
output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*compiler-rt.*")
def test_simultaneous_add_remove(self):
"""
Test that we error out when someone is trying to add and remove the same
project with the same command.
"""
# Try the really basic case and make sure we're not touching anything
with self.assertRaises(subprocess.CalledProcessError) as context:
self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--add",
"clang",
"--remove",
"clang"])
self.assertRegex(
str(context.exception.output),
"(.*\n)*.*Can't add and remove clang at the same time(.*\n)*")
self.assertFalse(
os.path.exists(
os.path.join(self.llvm_src, "tools", "clang")))
# Try something a bit more complicated and make sure we're not touching
# anything
self.__add_worktree(
self.__get_subproj_repo("lld"),
os.path.join(self.llvm_src, "tools", "lld"),
self.branch)
with self.assertRaises(subprocess.CalledProcessError) as context:
self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--add",
"clang",
"lld",
"libcxx",
"--remove",
"lld",
"libcxx"])
self.assertRegex(
str(context.exception.output),
"(.*\n)*" +
".*Can't add and remove (lld|libcxx) at the same time(.*\n)*")
# Make sure we didn't touch anything
self.assertFalse(
os.path.exists(
os.path.join(self.llvm_src, "tools", "clang")))
self.assertTrue(
os.path.exists(
os.path.join(self.llvm_src, "tools", "lld")))
self.assertFalse(
os.path.exists(
os.path.join(self.llvm_src, "projects", "libcxx")))
def test_multiple_adds_removes(self):
"""
Test that we can have multiple --add and --remove options in the same
command and that only the last one of each kind matters.
"""
output = self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--add",
"libcxxabi",
"--remove",
"lld",
"lldb",
"--add",
"clang",
"libcxx",
"--remove",
"libunwind"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*libcxx.*\n")
output = self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--add",
"libunwind",
"libcxxabi",
"--remove",
"clang",
"libcxx",
"--add",
"compiler-rt",
"--remove",
"libcxxabi"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*compiler-rt.*\n" +
".*libcxx.*\n")
output = self.run_with_output([self.python,
self.script,
self.env,
"projects",
"--add",
"libcxx",
"--remove",
"lld",
"--add",
"lld",
"--remove",
"libcxx"])
self.assertRegex(output,
"Projects linked:.*\n" +
".*clang.*\n" +
".*compiler-rt.*\n" +
".*lld.*\n")