mirror of https://github.com/sipwise/ngcpcfg.git
The old testsuite wasn't updated for way too long and since ngcpcfg receives more and more features we need a decent test coverage. pytest seems to provide the right level of abstraction, excellent fixtures and junit-xml reporting as needed. Inspired by Vincent Bernat's https://github.com/vincentbernat/lldpd/tree/master/tests/integration Thanks Victor Seva <vseva@sipwise.com>, Vincent Bernat <vincent@bernat.im>, Christian Hofstaedtler <christian@hofstaedtler.name> and Lukas Prokop <admin@lukas-prokop.at> for feedback, inspiration and help Change-Id: Iffed87e8cc540169bed89c00967a03e80859179echanges/20/7420/17
parent
e40ec0137e
commit
acfa825767
@ -0,0 +1,13 @@
|
||||
# cache files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# editor temp files
|
||||
*~
|
||||
.*.sw?
|
||||
.sw?
|
||||
\#*\#
|
||||
DEADJOE
|
||||
|
||||
# testing output
|
||||
results
|
@ -0,0 +1,36 @@
|
||||
# DOCKER_NAME=ngcpcfg-jessie
|
||||
FROM docker.mgm.sipwise.com/sipwise-jessie:latest
|
||||
|
||||
# Important! Update this no-op ENV variable when this Dockerfile
|
||||
# is updated with the current date. It will force refresh of all
|
||||
# of the base images and things like `apt-get update` won't be using
|
||||
# old cached versions when the Dockerfile is built.
|
||||
ENV REFRESHED_AT 2017-05-27
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# sourcecode test dependencies
|
||||
RUN apt-get install --assume-yes libyaml-tiny-perl netcat libhash-merge-perl libtemplate-perl libyaml-perl
|
||||
# misc stuff for execution/debugging/devel
|
||||
RUN apt-get install --assume-yes fakeroot git lsof make strace
|
||||
# test execution
|
||||
RUN apt-get install --assume-yes python3-pytest python3-junitxml
|
||||
|
||||
RUN echo './t/testrunner' >>/root/.bash_history
|
||||
|
||||
WORKDIR /code/
|
||||
|
||||
################################################################################
|
||||
# Instructions for usage
|
||||
# ----------------------
|
||||
# When you want to build the base image from scratch (jump to the next section if you don't want to build yourself!):
|
||||
# % docker build --tag="ngcpcfg-jessie" .
|
||||
# % docker run --rm -i -t -v $(pwd)/..:/code:rw ngcpcfg-jessie:latest bash
|
||||
#
|
||||
# Use the existing docker image:
|
||||
# % docker pull docker1.mgm.sipwise.com/ngcpcfg-jessie
|
||||
# % docker run --rm -i -t -v $(pwd)/..:/code:rw docker1.mgm.sipwise.com/ngcpcfg-jessie:latest bash
|
||||
#
|
||||
# Inside docker (the command is in history, just press UP button):
|
||||
# ./t/testrunner
|
||||
################################################################################
|
@ -0,0 +1,2 @@
|
||||
import pytest
|
||||
from fixtures import *
|
Binary file not shown.
@ -0,0 +1,17 @@
|
||||
# begin section managed by ngcpcfg (do not edit this section by hand)
|
||||
# new and old versions of conffiles, stored by dpkg
|
||||
*.dpkg-*
|
||||
# new and old versions of conffiles, stored by ucf
|
||||
*.ucf-*
|
||||
|
||||
# old versions of files
|
||||
*.old
|
||||
|
||||
# editor temp files
|
||||
*~
|
||||
.*.sw?
|
||||
.sw?
|
||||
\#*\#
|
||||
DEADJOE
|
||||
|
||||
# end section managed by ngcpcfg
|
@ -0,0 +1,4 @@
|
||||
from .commandline import *
|
||||
from .fs import *
|
||||
from .gitrepo import *
|
||||
from .programs import *
|
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo spce
|
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
|
||||
ProcessResult = collections.namedtuple('ProcessResult',
|
||||
['exitcode', 'stdout', 'stderr'])
|
||||
|
||||
|
||||
def run(*args, env=None, timeout=None, stdin=None, expect_stdout=True,
|
||||
expect_stderr=True, outencoding='utf-8', errencoding='utf-8'):
|
||||
"""Execute command in `args` in a subprocess"""
|
||||
|
||||
# drop stdout & stderr and spare some memory, if not needed
|
||||
stdout = subprocess.PIPE
|
||||
if not expect_stdout:
|
||||
stdout = None
|
||||
|
||||
stderr = subprocess.PIPE
|
||||
if not expect_stderr:
|
||||
stderr = None
|
||||
|
||||
# run command
|
||||
p = subprocess.Popen(args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env)
|
||||
if stdin:
|
||||
p.stdin.write(stdin)
|
||||
stdout, stderr = p.communicate(timeout=timeout)
|
||||
|
||||
# decode output
|
||||
if expect_stdout and stdout:
|
||||
stdoutput = stdout.decode(outencoding)
|
||||
else:
|
||||
stdoutput = ''
|
||||
if expect_stderr and stderr:
|
||||
stderror = stderr.decode(errencoding)
|
||||
else:
|
||||
stderror = ''
|
||||
|
||||
return ProcessResult(p.returncode, stdoutput, stderror)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cli():
|
||||
"""Run a command as subprocess.
|
||||
Returns a namedtuple with members (stdout, stderr, exitcode).
|
||||
|
||||
:param env: a dictionary of environment variables
|
||||
:param timeout: maximum number of seconds before
|
||||
TimeoutError is raised
|
||||
:param stdin: bytes-object to write to stdin once the
|
||||
program has started
|
||||
:param expect_stdout: shall stdout be provided in the result?
|
||||
:param outencoding: encoding to decode stdout
|
||||
:param expect_stderr: shall stderr be provided in the result?
|
||||
:param errencoding: encoding to decode stderr
|
||||
"""
|
||||
return run
|
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
import os.path
|
||||
import contextlib
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def keep_directory():
|
||||
"""Restore the current working directory when finished."""
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(cwd)
|
@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import pytest
|
||||
import tarfile
|
||||
import zipfile
|
||||
import tempfile
|
||||
|
||||
from .commandline import run
|
||||
from .fs import keep_directory
|
||||
|
||||
BUILTIN_GIT_COMMANDS = {"add", "am", "annotate", "apply", "archive",
|
||||
"bisect--helper", "blame", "branch",
|
||||
"bundle", "cat-file", "check-attr",
|
||||
"check-ignore", "check-mailmap",
|
||||
"check-ref-format", "checkout",
|
||||
"checkout-index", "cherry",
|
||||
"cherry-pick", "clean", "clone",
|
||||
"column", "commit", "commit-tree",
|
||||
"config", "count-objects", "credential",
|
||||
"describe", "diff", "diff-files",
|
||||
"diff-index", "diff-tree",
|
||||
"fast-export", "fetch", "fetch-pack",
|
||||
"fmt-merge-msg", "for-each-ref",
|
||||
"format-patch", "fsck", "fsck-objects",
|
||||
"gc", "get-tar-commit-id", "grep",
|
||||
"hash-object", "help", "index-pack",
|
||||
"init", "init-db", "interpret-trailers",
|
||||
"log", "ls-files", "ls-remote",
|
||||
"ls-tree", "mailinfo", "mailsplit",
|
||||
"merge", "merge-base", "merge-file",
|
||||
"merge-index", "merge-ours",
|
||||
"merge-recursive", "merge-subtree",
|
||||
"merge-tree", "mktag", "mktree", "mv",
|
||||
"name-rev", "notes", "pack-objects",
|
||||
"pack-redundant", "pack-refs",
|
||||
"patch-id", "prune", "prune-packed",
|
||||
"pull", "push", "read-tree",
|
||||
"receive-pack", "reflog", "remote",
|
||||
"remote-ext", "remote-fd", "remote-ftp",
|
||||
"remote-ftps", "remote-https", "repack",
|
||||
"replace", "rerere", "reset",
|
||||
"rev-list", "rev-parse", "revert", "rm",
|
||||
"send-pack", "shortlog", "show",
|
||||
"show-branch", "show-ref", "stage",
|
||||
"status", "stripspace",
|
||||
"submodule--helper", "symbolic-ref",
|
||||
"tag", "unpack-file", "unpack-objects",
|
||||
"update-index", "update-ref",
|
||||
"update-server-info", "upload-archive",
|
||||
"var", "verify-commit", "verify-pack",
|
||||
"verify-tag", "whatchanged", "worktree",
|
||||
"write-tree"}
|
||||
|
||||
|
||||
# helpers
|
||||
|
||||
def create_tmp_folder():
|
||||
"""Create and return a unique temporary folder"""
|
||||
return tempfile.mkdtemp(prefix='ngcp-', suffix='-pytest')
|
||||
|
||||
|
||||
def delete_tmp_folder(folder):
|
||||
"""Recursively delete a folder"""
|
||||
shutil.rmtree(folder)
|
||||
|
||||
|
||||
def find_git_root(folder):
|
||||
"""If `folder` contains one folder,
|
||||
return this folder. Otherwise `folder`"""
|
||||
children = os.listdir(folder)
|
||||
if len(children) > 1:
|
||||
return folder
|
||||
else:
|
||||
return os.path.join(folder, children[0])
|
||||
|
||||
|
||||
# implementation
|
||||
|
||||
class GitCommand:
|
||||
def __init__(self, repo, command):
|
||||
self.repo = repo
|
||||
self.command = command
|
||||
|
||||
def __call__(self, *args):
|
||||
with keep_directory():
|
||||
os.chdir(self.repo.root)
|
||||
return run('git', self.command, *args)
|
||||
|
||||
def __repr__(self):
|
||||
return "command 'git {}'".format(self.command)
|
||||
|
||||
|
||||
class GitRepository:
|
||||
"""Represents a git repository we are working with"""
|
||||
|
||||
def __init__(self, root, delete_fn=None):
|
||||
self.root = root
|
||||
self.delete_fn = delete_fn
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in BUILTIN_GIT_COMMANDS:
|
||||
raise ValueError("git command '{}' is unknown".format(name))
|
||||
return GitCommand(self, name)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_details):
|
||||
if self.delete_fn:
|
||||
self.delete_fn()
|
||||
|
||||
@property
|
||||
def branch(self):
|
||||
"""Return current git branch"""
|
||||
ex, out, err = GitCommand(self, 'branch')()
|
||||
assert ex == 0
|
||||
for line in out.splitlines():
|
||||
if line.startswith('*'):
|
||||
return line[1:].strip()
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""Return git version in use"""
|
||||
ex, out, err = GitCommand(self, '--version')()
|
||||
assert ex == 0
|
||||
return ' '.join(out.strip().split()[2:])
|
||||
|
||||
def __repr__(self):
|
||||
return "GitRepository at '{}'".format(self.root)
|
||||
|
||||
|
||||
class GitLoader:
|
||||
"""Provides methods to download a git repository"""
|
||||
default = 'default-git-repository.tar.gz'
|
||||
|
||||
def __init__(self):
|
||||
self.localpath = create_tmp_folder()
|
||||
|
||||
@staticmethod
|
||||
def extract_archive(src, dest):
|
||||
"""Extract files and folder in archive `src` to path `dest`"""
|
||||
suffix = None
|
||||
if src.endswith('.tgz') or src.endswith('.tar.gz'):
|
||||
suffix = ':gz'
|
||||
elif src.endswith('.tbz2') or src.endswith('.tar.bz2'):
|
||||
suffix = ':bz2'
|
||||
elif src.endswith('.tar.lzma'):
|
||||
suffix = ':xz'
|
||||
elif src.endswith('.tar'):
|
||||
suffix = ''
|
||||
|
||||
if suffix is not None:
|
||||
ar = tarfile.open(src, 'r' + suffix)
|
||||
ar.extractall(path=dest)
|
||||
return dest
|
||||
elif src.endswith('.zip'):
|
||||
with zipfile.ZipFile(src, 'r') as fd:
|
||||
fd.extractall(path=dest)
|
||||
return dest
|
||||
else:
|
||||
raise ValueError('Archive of unknown file type: {}'.format(src))
|
||||
|
||||
def from_url(self, url):
|
||||
"""Clone git repository from URL"""
|
||||
ex, out, err = cli.run('git', 'clone', url, self.localpath)
|
||||
assert ex == 0
|
||||
return GitRepository(find_git_root(self.localpath),
|
||||
delete_fn=self.cleanup)
|
||||
|
||||
def from_archive(self, archive_path):
|
||||
"""Extract git repository from given archive"""
|
||||
self.localpath = self.extract_archive(archive_path, self.localpath)
|
||||
return GitRepository(find_git_root(self.localpath),
|
||||
delete_fn=self.cleanup)
|
||||
|
||||
def in_folder(self, path):
|
||||
"""Assume `path` already contains a git repository"""
|
||||
assert os.path.exists(os.path.join(path, '.git')), \
|
||||
'.git folder must exist in git repository'
|
||||
return GitRepository(path)
|
||||
|
||||
def cleanup(self):
|
||||
delete_tmp_folder(self.localpath)
|
||||
|
||||
def __repr__(self):
|
||||
return "GitLoader for '{}'".format(self.localpath)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gitrepo():
|
||||
return GitLoader()
|
@ -0,0 +1,37 @@
|
||||
# directory name where ngcpcfg is managed through git
|
||||
NGCPCTL_BASE="$(pwd)/"
|
||||
NGCPCTL_MAIN="${NGCPCTL_BASE}/fixtures/repos/"
|
||||
NGCPCTL_CONFIG="${NGCPCTL_MAIN}/config.yml"
|
||||
HOST_CONFIG="${NGCPCTL_MAIN}/config.$(hostname).yml"
|
||||
LOCAL_CONFIG="${NGCPCTL_MAIN}/config.local.yml"
|
||||
CONSTANTS_CONFIG="${NGCPCTL_MAIN}/constants.yml"
|
||||
NETWORK_CONFIG="${NGCPCTL_MAIN}/network.yml"
|
||||
RTP_INTERFACES_CONFIG="/etc/ngcp-rtpengine-daemon/interfaces.yml"
|
||||
EXTRA_CONFIG_DIR="${NGCPCTL_MAIN}/config.d/"
|
||||
|
||||
# configuration dirs that should be managed
|
||||
CONFIG_POOL='/etc /var'
|
||||
|
||||
# location of templates
|
||||
TEMPLATE_POOL_BASE="${NGCPCTL_MAIN}/templates"
|
||||
|
||||
# location of service definitions
|
||||
SERVICES_POOL_BASE="${NGCPCTL_MAIN}/templates"
|
||||
|
||||
# Backward compatibility config for upgrade mr3.4*->mr3.5*
|
||||
# it can be removed when the next LTS is released:
|
||||
TEMPLATE_POOL="${TEMPLATE_POOL_BASE}/etc"
|
||||
SERVICES_POOL="${SERVICES_POOL_BASE}/etc"
|
||||
|
||||
# timestamp format for console output
|
||||
TIME_FORMAT="+%F %T"
|
||||
|
||||
# Run-time state directory
|
||||
RUN_DIR='/var/run'
|
||||
|
||||
# directory holding files for internal state of ngcpcfg
|
||||
STATE_FILES_DIR='/var/lib/ngcpcfg/state/'
|
||||
|
||||
# validate configs using kwalify schema
|
||||
VALIDATE_SCHEMA="false"
|
||||
## END OF FILE #################################################################
|
@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ngcpcfgcli(tmpdir, *args):
|
||||
"""Execute ``ngcpcfg``."""
|
||||
|
||||
def run(*args, env={}):
|
||||
testenv = {
|
||||
'PATH': 'fixtures/bin:/usr/bin:/bin:/usr/sbin:/sbin',
|
||||
'FUNCTIONS': '../functions/',
|
||||
'NGCPCFG': 'fixtures/ngcpcfg.cfg',
|
||||
'SCRIPTS': '../scripts/',
|
||||
'HELPER': '../helper/',
|
||||
'NGCP_PORTFILE': '/tmp/ngcpcfg.port',
|
||||
}
|
||||
testenv.update(env)
|
||||
|
||||
# if we're already running under root don't execute under fakeroot,
|
||||
# causing strange problems when debugging execution e.g. via strace
|
||||
if os.getuid() == 0:
|
||||
fakeroot = []
|
||||
else:
|
||||
fakeroot = ['fakeroot']
|
||||
|
||||
p = subprocess.Popen(fakeroot + ['../sbin/ngcpcfg'] + list(args),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, env=testenv)
|
||||
stdout, stderr = p.communicate(timeout=30)
|
||||
stdout, stderr = str(stdout), str(stderr)
|
||||
result = namedtuple('ProcessResult',
|
||||
['returncode', 'stdout', 'stderr'])(
|
||||
p.returncode, stdout, stderr)
|
||||
return result
|
||||
|
||||
return run
|
@ -0,0 +1,2 @@
|
||||
---
|
||||
tests:
|
@ -0,0 +1,2 @@
|
||||
---
|
||||
tests:
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
hosts:
|
||||
self:
|
@ -0,0 +1,2 @@
|
||||
[% TAGS [- -] %]
|
||||
APT::Install-Recommends "0";
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
python_files=*.py
|
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env py.test-3
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import re
|
||||
|
||||
|
||||
def test_add_file_to_default_repo(cli, gitrepo):
|
||||
src = 'default-git-repository.tar.gz'
|
||||
|
||||
with gitrepo.from_archive(src) as git:
|
||||
# configure git user
|
||||
git.config('--local', 'user.email', 'me@example.com')
|
||||
git.config('--local', 'user.name', 'pytest robot')
|
||||
print('Using git {}'.format(git.version))
|
||||
|
||||
# git status
|
||||
ex, out, err = git.status()
|
||||
assert ex == 0
|
||||
assert 'On branch' in out
|
||||
|
||||
# create new file
|
||||
newfile = os.path.join(git.root, 'newfile')
|
||||
with open(newfile, 'w') as fd:
|
||||
fd.write('#!/bin/sh\necho "Hello World"\n')
|
||||
|
||||
# adding file to repository
|
||||
assert git.branch == 'master'
|
||||
git.add(newfile)
|
||||
git.commit('-m', 'Adding a new file')
|
||||
|
||||
# checking log
|
||||
ex, out, err = git.log()
|
||||
assert ex == 0
|
||||
assert 'Adding a new file' in out
|
||||
|
||||
|
||||
@pytest.mark.tt_11776
|
||||
def test_status_output(cli, gitrepo):
|
||||
# we mock an existing repository by loading it from the default archive
|
||||
with gitrepo.from_archive(gitrepo.default) as git:
|
||||
# now we work with "existing" repository with path given in git.root
|
||||
with gitrepo.in_folder(git.root) as git:
|
||||
ex, out, err = git.status()
|
||||
assert ex == 0
|
||||
# git >=v2.9.3 uses "working tree",
|
||||
# before it was "working directory"
|
||||
regex = re.compile(r"nothing to commit, working (tree|directory)")
|
||||
assert re.search(regex, out)
|
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env py.test-3
|
||||
|
||||
import pytest
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
|
||||
@pytest.mark.cmdline
|
||||
def test_ngcpcfgcfg_ok(ngcpcfgcli):
|
||||
out = ngcpcfgcli()
|
||||
assert 'For further usage information and options ' \
|
||||
'see the ngcpcfg(8) man page' in out.stderr
|
||||
|
||||
|
||||
@pytest.mark.cmdline
|
||||
def test_ngcpcfgcfg_ko(ngcpcfgcli):
|
||||
out = ngcpcfgcli(env={'NGCPCFG': '/doesnotexist'})
|
||||
assert 'Error: Could not read configuration file ' \
|
||||
'/etc/ngcp-config/ngcpcfg.cfg. Exiting.' in out.stderr
|
||||
|
||||
|
||||
# NOTE - this one fails if the *main* ngcpcfg.git is not
|
||||
# standing on master branch, therefore use --ignore-branch-check
|
||||
# until we've a mock/fixture for it
|
||||
@pytest.mark.mt_16391
|
||||
def test_simple_build_template_ok(ngcpcfgcli):
|
||||
tmpdir = tempfile.mkdtemp(prefix='ngcp-', suffix='-pytest-output')
|
||||
out = ngcpcfgcli("build", "--ignore-branch-check",
|
||||
"/etc/apt/apt.conf.d/71_no_recommended",
|
||||
env={
|
||||
'NGCP_PORTFILE': '/tmp/ngcpcfg.port',
|
||||
'OUTPUT_DIRECTORY': tmpdir,
|
||||
})
|
||||
regex = re.compile(r"Generating " +
|
||||
tmpdir +
|
||||
r"//etc/apt/apt.conf.d/71_no_recommended: OK")
|
||||
assert re.search(regex, out.stdout)
|
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# This script is used for running the tests with proper arguments
|
||||
# from within Jenkins
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
if [ -d /results ] ; then
|
||||
# Running from Jenkins (RW)
|
||||
RESULTS="/results"
|
||||
|
||||
cd "/code"
|
||||
else
|
||||
# Running locally in Docker
|
||||
RESULTS="./results"
|
||||
mkdir -p "${RESULTS}"
|
||||
fi
|
||||
|
||||
RESULTS=${RESULTS} make test
|
Loading…
Reference in new issue