[reprotest] 02/02: Support substractable parameters for variations, progress towards > 2 builds

Ximin Luo infinity0 at debian.org
Wed Sep 13 11:43:10 UTC 2017


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch master
in repository reprotest.

commit a22e52bad6b7a1f0703e438195ebeeab2d190aa1
Author: Ximin Luo <infinity0 at debian.org>
Date:   Wed Sep 13 13:41:59 2017 +0200

    Support substractable parameters for variations, progress towards > 2 builds
---
 README.rst                            |   6 +-
 debian/changelog                      |   4 +
 reprotest/__init__.py                 |  44 ++++----
 reprotest/build.py                    | 135 ++++++++++++++++++-----
 reprotest/mdiffconf.py                | 194 ++++++++++++++++++++++++++++++++++
 tests/test_mdiffconf.py               |  19 ++++
 tests/{tests.py => test_reprotest.py} |   8 +-
 tox.ini                               |   2 +-
 8 files changed, 353 insertions(+), 59 deletions(-)

diff --git a/README.rst b/README.rst
index 04faecc..d3cbed1 100644
--- a/README.rst
+++ b/README.rst
@@ -208,9 +208,11 @@ your use-case::
     $ cat <<EOF | sudo tee -a /etc/sudoers.d/local-reprotest
     $USER ALL = ($OTHERUSER) NOPASSWD: ALL
     $USER ALL = NOPASSWD: /bin/chown -h -R --from=$OTHERUSER $USER /tmp/autopkgtest.$a$a$a$a$a$a/const_build_path/
-    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$OTHERUSER $USER /tmp/autopkgtest.$a$a$a$a$a$a/experiment/
+    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$OTHERUSER $USER /tmp/autopkgtest.$a$a$a$a$a$a/build-experiment/
+    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$OTHERUSER $USER /tmp/autopkgtest.$a$a$a$a$a$a/build-experiment-before-disorderfs/
     $USER ALL = NOPASSWD: /bin/chown -h -R --from=$USER $OTHERUSER /tmp/autopkgtest.$a$a$a$a$a$a/const_build_path/
-    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$USER $OTHERUSER /tmp/autopkgtest.$a$a$a$a$a$a/experiment/
+    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$USER $OTHERUSER /tmp/autopkgtest.$a$a$a$a$a$a/build-experiment/
+    $USER ALL = NOPASSWD: /bin/chown -h -R --from=$USER $OTHERUSER /tmp/autopkgtest.$a$a$a$a$a$a/build-experiment-before-disorderfs/
     EOF
 
 Repeat this for each user you'd like to use. Obviously, don't pick a privileged
diff --git a/debian/changelog b/debian/changelog
index e48a9c4..e841fd1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,11 +1,15 @@
 reprotest (0.7) UNRELEASED; urgency=medium
 
+  FIXME do not release
+  - update the docs
+
   [ Ximin Luo ]
   * Document when one should use --diffoscope-args=--exclude-directory-metadata
     and do this in our Debian package presets.
   * Bump diffoscope Recommends version to >= 84 to support this flag.
   * Import autopkgtest 4.4, with minimal patches.
   * Choose an existent HOME for the control build. (Closes: #860428)
+  * Add the ability to vary the user (Closes: #872412)
 
   [ Mattia Rizzolo ]
   * Bump Standards-Version to 4.0.0.
diff --git a/reprotest/__init__.py b/reprotest/__init__.py
index 487b242..e9a07b6 100644
--- a/reprotest/__init__.py
+++ b/reprotest/__init__.py
@@ -20,7 +20,7 @@ import pkg_resources
 
 from reprotest.lib import adtlog
 from reprotest.lib import adt_testbed
-from reprotest.build import Build, VARIATIONS, VariationContext
+from reprotest.build import Build, VariationSpec, Variations
 from reprotest import presets
 
 
@@ -105,13 +105,15 @@ class BuildContext(collections.namedtuple('_BuildContext', 'testbed_root local_d
             tree = self.testbed_src
         )
 
-    def plan_variations(self, build, is_control, variation_context, variations):
-        actions = [(not is_control and v in variations, v) for v in VARIATIONS]
+    def plan_variations(self, build, is_control, variations): # XXX
+        if is_control:
+            variations = variations._replace(spec=VariationSpec.empty())
+        actions = variations.spec.actions()
         logging.info('build "%s": %s',
             self.build_name,
-            ", ".join("%s %s" % ("FIX" if not vary else "vary", v) for vary, v in actions))
-        for vary, v in actions:
-            build = VARIATIONS[v](variation_context, build, vary)
+            ", ".join("%s %s" % ("FIX" if not vary else "vary", v) for v, vary, action in actions))
+        for v, vary, action in actions:
+            build = action(variations, build, vary)
         return build
 
     def copydown(self, testbed):
@@ -175,7 +177,7 @@ def run_diff(dist_0, dist_1, diffoscope_args, store_dir):
 
 def check(build_command, artifact_pattern, virtual_server_args, source_root,
           no_clean_on_error=False, store_dir=None, diffoscope_args=[],
-          variations=VARIATIONS, variation_context=VariationContext.default(),
+          variations=Variations.default(),
           testbed_pre=None, testbed_init=None, host_distro='debian'):
     # default argument [] is safe here because we never mutate it.
     if not source_root:
@@ -201,7 +203,7 @@ def check(build_command, artifact_pattern, virtual_server_args, source_root,
             source_root = new_source_root
         logging.debug("source_root: %s", source_root)
 
-        variation_context = variation_context.guess_default_faketime(source_root)
+        variations = variations._replace(spec=variations.spec.apply_dynamic_defaults(source_root))
 
         # TODO: an alternative strategy is to run the testbed many times, one for each build
         # not sure if it's worth implementing at this stage, but perhaps in the future.
@@ -221,7 +223,7 @@ def check(build_command, artifact_pattern, virtual_server_args, source_root,
                 for bctx in build_contexts]
 
             logging.log(5, "builds: %r", builds)
-            builds = [c.plan_variations(b, c.build_name == "control", variation_context, variations)
+            builds = [c.plan_variations(b, c.build_name == "control", variations)
                 for c, b in zip(build_contexts, builds)]
             logging.log(5, "builds: %r", builds)
 
@@ -351,20 +353,14 @@ def cli_parser():
         help='Save the artifacts in this directory, which must be empty or '
         'non-existent. Otherwise, the artifacts will be deleted and you only '
         'see their hashes (if reproducible) or the diff output (if not).')
-    group1.add_argument('--variations', default=frozenset(VARIATIONS.keys()),
-        type=lambda s: frozenset(sss for ss in s.split(',') for sss in ss.split()),
+    group1.add_argument('--variations', default=["+all"], action='append',
         help='Build variations to test as a whitespace-or-comma-separated '
-        'list.  Default is to test all available variations: %s.' %
-        ', '.join(VARIATIONS.keys()))
-    group1.add_argument('--dont-vary', default=frozenset(),
-        type=lambda s: frozenset(sss for ss in s.split(',') for sss in ss.split()),
+        'list.  Default is to test all available variations: %(default)s.')
+    # TODO: hide this from --help, and deprecate it
+    group1.add_argument('--dont-vary', default=[], action='append',
         help='Build variations *not* to test as a whitespace-or-comma-separated '
         'list.  These take precedence over what you set for --variations. '
         'Default is nothing, i.e. test whatever you set for --variations.')
-    group1.add_argument('--user-groups', default=frozenset(),
-        type=lambda s: frozenset(tuple(x.split(':',1)) for x in s.split(',')),
-        help='Comma-separated list of possible user:group combinations which '
-        'will be randomly chosen to `sudo -i` to when varying user_group.')
 
     group2 = parser.add_argument_group('diff options')
     group2.add_argument('--diffoscope-arg', default=[], action='append',
@@ -505,11 +501,9 @@ def run(argv, check):
             diffoscope_args = values.diffoscope_args + diffoscope_args
 
     # Variations args
-    variations = parsed_args.variations - parsed_args.dont_vary
-    _ = VariationContext.default()
-    _ = _._replace(verbosity=verbosity)
-    _ = _._replace(user_groups=_.user_groups | parsed_args.user_groups)
-    variation_context = _
+    variations = parsed_args.variations + ["-%s" % a for x in parsed_args.dont_vary for a in x.split(",")]
+    spec = VariationSpec().extend(variations)
+    variations = Variations(verbosity, spec)
 
     # Remaining args
     host_distro = parsed_args.host_distro
@@ -525,7 +519,7 @@ def run(argv, check):
     check_args_keys = (
         "build_command", "artifact_pattern", "virtual_server_args", "source_root",
         "no_clean_on_error", "store_dir", "diffoscope_args",
-        "variations", "variation_context",
+        "variations",
         "testbed_pre", "testbed_init", "host_distro")
     l = locals()
     check_args = collections.OrderedDict([(k, l[k]) for k in check_args_keys])
diff --git a/reprotest/build.py b/reprotest/build.py
index 392dc66..072dad7 100644
--- a/reprotest/build.py
+++ b/reprotest/build.py
@@ -1,13 +1,18 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/gpl-3.0.en.html
+# For details: reprotest/debian/copyright
+
 import collections
 import getpass
 import grp
 import logging
 import os
+import shlex
 import random
 import time
 import types
 
 from reprotest import _shell_ast
+from reprotest import mdiffconf
 
 
 def dirname(p):
@@ -141,21 +146,6 @@ fi
             return str(subshell)
 
 
-class VariationContext(collections.namedtuple('_VariationContext', 'verbosity user_groups default_faketime')):
-
-    @classmethod
-    def default(cls):
-        return cls(0, frozenset(), 0)
-
-    def guess_default_faketime(self, source_root):
-        # Get the latest modification date of all the files in the source root.
-        # This tries hard to avoid bad interactions with faketime and make(1) etc.
-        # However if you're building this too soon after changing one of the source
-        # files then the effect of this variation is not very great.
-        filemtimes = (os.path.getmtime(os.path.join(root, f)) for root, dirs, files in os.walk(source_root) for f in files)
-        return self._replace(default_faketime=int(max(filemtimes, default=0)))
-
-
 # time zone, locales, disorderfs, host name, user/group, shell, CPU
 # number, architecture for uname (using linux64), umask, HOME, see
 # also: https://tests.reproducible-builds.org/index_variations.html
@@ -272,10 +262,12 @@ def timezone(ctx, build, vary):
 
 def faketime(ctx, build, vary):
     if not vary:
+        # FIXME: this does not actually fix the time, it just lets the system clock run normally
         return build
-    lastmt = ctx.default_faketime
+    lastmt = random.choice(ctx.spec.time.faketimes)
     now = time.time()
-    if lastmt < now - 32253180:
+    # FIXME: better way of choosing which faketime to use
+    if lastmt.startswith("@") and int(lastmt[1:]) < now - 32253180:
         # if lastmt is far in the past, use that, it's a bit safer
         faket = '@%s' % lastmt
     else:
@@ -299,14 +291,26 @@ def user_group(ctx, build, vary):
     if not vary:
         return build
 
-    if not ctx.user_groups:
-        logging.warn("IGNORING user_group variation, because no --user-groups were given. To suppress this warning, give --dont-vary user_group")
+    if not ctx.spec.user_group.available:
+        logging.warn("IGNORING user_group variation; supply more usergroups "
+        "with --variations=user_group.available+=USER1:GROUP1;USER2:GROUP2 or "
+        "alternatively, suppress this warning with --variations=-user_group")
         return build
 
     olduser = getpass.getuser()
     oldgroup = grp.getgrgid(os.getgid()).gr_name
-    user, group = random.choice(list(set(ctx.user_groups) - set([(olduser, oldgroup)])))
-    sudobuild = _shell_ast.SimpleCommand.make('sudo', '-E', '-u', user, '-g', group)
+    user_group = random.choice(list(set(ctx.spec.user_group.available) - set([(olduser, oldgroup)])))
+    if ":" in user_group:
+        user, group = user_group.split(":", 1)
+        if user:
+            sudo_command = ('sudo', '-E', '-u', user, '-g', group)
+        else:
+            user = olduser
+            sudo_command = ('sudo', '-E', '-g', group)
+    else:
+        user = user_group # "user" is used below
+        sudo_command = ('sudo', '-E', '-u', user)
+    sudobuild = _shell_ast.SimpleCommand.make(*sudo_command)
     binpath = os.path.join(dirname(build.tree), 'bin')
 
     _ = build.append_to_build_command(sudobuild)
@@ -314,17 +318,18 @@ def user_group(ctx, build, vary):
     # we prefer that to running it as root, principle of least-privilege.
     _ = _.append_setup_exec('sh', '-ec', r'''
 mkdir "{0}"
-printf '#!/bin/sh\nsudo -u "{1}" -g "{2}" /usr/bin/disorderfs "$@"\n' > "{0}"/disorderfs
+printf '#!/bin/sh\n{1} /usr/bin/disorderfs "$@"\n' > "{0}"/disorderfs
 chmod +x "{0}"/disorderfs
-printf '#!/bin/sh\nsudo -u "{1}" -g "{2}" /bin/mkdir "$@"\n' > "{0}"/mkdir
+printf '#!/bin/sh\n{1} /bin/mkdir "$@"\n' > "{0}"/mkdir
 chmod +x "{0}"/mkdir
-printf '#!/bin/sh\nsudo -u "{1}" -g "{2}" /bin/fusermount "$@"\n' > "{0}"/fusermount
+printf '#!/bin/sh\n{1} /bin/fusermount "$@"\n' > "{0}"/fusermount
 chmod +x "{0}"/fusermount
-'''.format(binpath, user, group))
+'''.format(binpath, " ".join(map(shlex.quote, sudo_command))))
     _ = _.append_setup_exec_raw('export', 'PATH="%s:$PATH"' % binpath)
-    _ = _.append_setup_exec('sudo', 'chown', '-h', '-R', '--from=%s' % olduser, user, build.tree)
-    # TODO: artifacts probably shouldn't be chown'd back
-    _ = _.prepend_cleanup_exec('sudo', 'chown', '-h', '-R', '--from=%s' % user, olduser, build.tree)
+    if user != olduser:
+        _ = _.append_setup_exec('sudo', 'chown', '-h', '-R', '--from=%s' % olduser, user, build.tree)
+        # TODO: artifacts probably shouldn't be chown'd back
+        _ = _.prepend_cleanup_exec('sudo', 'chown', '-h', '-R', '--from=%s' % user, olduser, build.tree)
     return _
 
 
@@ -347,3 +352,77 @@ VARIATIONS = collections.OrderedDict([
     ('timezone', timezone),
     ('umask', umask),
 ])
+
+
+class TimeVariation(collections.namedtuple('_TimeVariation', 'faketimes auto_faketimes')):
+    @classmethod
+    def default(cls):
+        return cls(mdiffconf.strlist_set(";"), mdiffconf.strlist_set(";", ['SOURCE_DATE_EPOCH']))
+
+    @classmethod
+    def empty(cls):
+        return cls(mdiffconf.strlist_set(";"), mdiffconf.strlist_set(";"))
+
+    def apply_dynamic_defaults(self, source_root):
+        new_faketimes = []
+        for a in self.auto_faketimes:
+            if a == "SOURCE_DATE_EPOCH":
+                # Get the latest modification date of all the files in the source root.
+                # This tries hard to avoid bad interactions with faketime and make(1) etc.
+                # However if you're building this too soon after changing one of the source
+                # files then the effect of this variation is not very great.
+                filemtimes = (os.path.getmtime(os.path.join(root, f)) for root, dirs, files in os.walk(source_root) for f in files)
+                new_faketimes.append("@%d" % int(max(filemtimes, default=0)))
+            else:
+                raise ValueError("unrecognized auto_faketime: %s" % a)
+        return self.empty()._replace(faketimes=self.faketimes + new_faketimes)
+
+
+class UserGroupVariation(collections.namedtuple('_UserGroupVariation', 'available')):
+    @classmethod
+    def default(cls):
+        return cls(mdiffconf.strlist_set(";"))
+
+
+class VariationSpec(mdiffconf.ImmutableNamespace):
+    @classmethod
+    def default(cls, variations=VARIATIONS):
+        default_overrides = {
+            "user_group": UserGroupVariation.default(),
+            "time": TimeVariation.default(),
+        }
+        return cls(**{k: default_overrides.get(k, True) for k in variations})
+
+    @classmethod
+    def empty(cls):
+        return cls()
+
+    aliases = { ("@+-", "all"): list(VARIATIONS.keys()) }
+    def extend(self, actions):
+        one = self.default()
+        return mdiffconf.parse_all(self, actions, one, one, self.aliases, sep=",")
+
+    def actions(self):
+        return [(k, k in self.__dict__, v) for k, v in VARIATIONS.items()]
+
+    def apply_dynamic_defaults(self, source_root):
+        return self.__class__(**{
+            k: v.apply_dynamic_defaults(source_root) if hasattr(v, "apply_dynamic_defaults") else v
+            for k, v in self.__dict__.items()
+        })
+
+
+class Variations(collections.namedtuple('_Variations', 'verbosity spec')):
+    @classmethod
+    def default(cls, *args, **kwargs):
+        return cls(0, VariationSpec.default(*args, **kwargs))
+
+
+if __name__ == "__main__":
+    import sys
+    d = VariationSpec()
+    for s in sys.argv[1:]:
+        d = d.append(s)
+        print(s)
+        print(">>>", d)
+    print("result", d.apply_dynamic_defaults("."))
diff --git a/reprotest/mdiffconf.py b/reprotest/mdiffconf.py
new file mode 100644
index 0000000..98775ae
--- /dev/null
+++ b/reprotest/mdiffconf.py
@@ -0,0 +1,194 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/gpl-3.0.en.html
+# For details: reprotest/debian/copyright
+"""Merge-diff config.
+
+This module implements a basic generic language for representing an object
+tree, described via a sequence of addition and subtraction operations on
+different parts of the tree, that are merged together to form the new tree.
+
+This is useful for manually specifying object trees in a compact way. This
+saves human users time instead of typing the same thing many many times.
+
+This module is totally independent of reprotest.
+"""
+
+import collections
+import functools
+import re
+import sys
+import types
+
+
+def rgetattr2(obj, attr, one=None):
+    if one is not None:
+        def _getattr2(arg, attr):
+            obj, one = arg
+            val = getattr(one, attr)
+            return (getattr(obj, attr) if hasattr(obj, attr) else val), val
+    else:
+        def _getattr2(arg, attr):
+            obj, one = arg
+            val = getattr(obj, attr)
+            return val, val
+    return functools.reduce(_getattr2, attr.split('.') if attr else [], (obj, one))
+
+def rsetattr(obj, attr, val, one=None):
+    pre, _, post = attr.rpartition('.')
+    target, target_one = rgetattr2(obj, pre, one) if pre else (obj, one)
+    target = target._replace(**{post: val})
+    return rsetattr(obj, pre, target) if pre else target
+
+def rdelattr(obj, attr, zero=None):
+    pre, _, post = attr.rpartition('.')
+    target, target_zero = rgetattr2(obj, pre, zero) if pre else (obj, zero)
+    try:
+        target = target._delete(post)
+    except AttributeError:
+        if zero is not None:
+            target = target._replace(**{post: rgetattr2(target_zero, post)[0]})
+        else:
+            raise
+    return rsetattr(obj, pre, target) if pre else target
+
+class ImmutableNamespace(types.SimpleNamespace):
+    def _replace(self, **kwargs):
+        new = self.__dict__.copy()
+        new.update(**kwargs)
+        return self.__class__(**new)
+
+    def _delete(self, name):
+        new = self.__dict__.copy()
+        if name in new:
+            del new[name]
+        return self.__class__(**new)
+
+def strlist_set(sep, value=[]):
+    class strlist_set(list):
+        def __init__(self, value=[]):
+            if isinstance(value, str):
+                value = value.split(sep) if value else []
+            return super().__init__(value)
+
+        def dedup(self, seq):
+            seen = set()
+            seen_add = seen.add
+            return self.__class__([x for x in seq if not (x in seen or seen_add(x))])
+
+        def __add__(self, value):
+            return self.dedup(super().__add__(value))
+
+        def __iadd__(self, value):
+            return self.__add__(value)
+
+        def __sub__(self, value):
+            return self.dedup([x for x in self if x not in set(value)])
+
+        def __isub__(self, value):
+            return self.__sub__(value)
+
+        def sep(self):
+            return sep
+    return strlist_set(value)
+
+def parse(d, action, one, zero=None, aliases={}):
+    """Parse an action, apply it to an object and return the new value.
+
+    Args:
+
+    obj: The top-level object to apply the action to.
+
+    action: The action to apply, specified as a string. The string is split on
+        the leftmost occurence of any supported $operator; everything to the
+        left of it is the $attribute and to the right of it is the $operand.
+
+        $attribute is in dotted-notation and specifies which $target attribute
+        (or sub-attribute, etc) of the $obj to apply $operator and $operand to.
+        If $attribute is the empty string, we apply these to $obj itself. If
+        the actual attribute does not exist on $obj, we implicitly add it as if
+        it had just been added with the "+" operator.
+
+        Supported operators:
+        +
+            Add the attribute named $operand to the $target. The value of the
+            attribute is taken from the $one default object. However if the
+            attribute is already on $target, it retains its existing value.
+        -
+            Remove the attribute named $operand from the $target. If it does
+            not support removing attributes, instead we set its value to that
+            taken from the $zero default object.
+        @
+            Re-set the attribute to its default value from $one. This is just a
+            shortcut for doing - and then +.
+        =, +=, -=
+            Apply the $operand to $target using the normal Python =, += and -=
+            operators respectively. If $operand is not of the same type as
+            $target, it is first coerced into it, using its class constructor.
+        ++, --
+            Apply 1 to the $target using the operators +=, -= respectively.
+
+    one: Having the same structure as $obj, this is used for default values
+        when adding new attributes; see "+" operator.
+
+    zero: Having the same structure as $obj. this is used for default values
+        when it is not possible to remove an attribute; see "-" operator.
+
+    aliases: A dictionary specifying aliases for various operations. Currently
+        supports the following keys:
+
+        ("@+-", $alias_operand): $real_operands:
+            When the operator is one of "@+-", this allows the user to use a
+            single $alias_operand to specify one-or-more $real_operands to
+            actually apply the operator to.
+    """
+    parts = re.split(r"(\+=|-=|\+\+|--|=|\+|-|@)", action, 1)
+    attr, op, operand = ("", "+", parts[0]) if len(parts) == 1 else parts
+    target, target_one = rgetattr2(d, attr, one)
+
+    if op in ("++", "--"):
+        if operand:
+            raise ValueError("action %s should have no operand: %s" % (action, operand))
+    else:
+        if not operand:
+            raise ValueError("action %s has no operand" % action)
+
+    operands = aliases.get(("@+-", operand), [operand]) if op in "@+-" else [operand]
+    for operand in operands:
+        if op == "@":
+            # equivalent to - then +
+            target = rdelattr(target, operand, rgetattr2(zero, attr)[0])
+            target = rsetattr(target, operand, rgetattr2(target, operand, target_one)[0])
+        elif op == "-":
+            target = rdelattr(target, operand, rgetattr2(zero, attr)[0])
+        elif op == "+":
+            target = rsetattr(target, operand, rgetattr2(target, operand, target_one)[0])
+        elif op == "=":
+            if not isinstance(operand, target.__class__):
+                operand = target.__class__(operand)
+            target = operand
+        elif op == "+=":
+            if not isinstance(operand, target.__class__):
+                operand = target.__class__(operand)
+            target += operand
+        elif op == "-=":
+            if not isinstance(operand, target.__class__):
+                operand = target.__class__(operand)
+            target -= operand
+        elif op == "++":
+            target += 1
+        elif op == "--":
+            target -= 1
+        else:
+            assert False
+
+    return rsetattr(d, attr, target, one) if attr else target
+
+def parse_all(d, actions, one, zero=None, aliases={}, sep=None):
+    """Parse a list of actions.
+
+    If sep is given, each list element is further split on that separator.
+    """
+    if isinstance(actions, str):
+        actions = [actions]
+    if sep:
+        actions = [a for aa in actions for a in aa.split(sep)]
+    return functools.reduce(lambda d, a: parse(d, a, one, zero, aliases), actions, d)
diff --git a/tests/test_mdiffconf.py b/tests/test_mdiffconf.py
new file mode 100644
index 0000000..e781aff
--- /dev/null
+++ b/tests/test_mdiffconf.py
@@ -0,0 +1,19 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/gpl-3.0.en.html
+# For details: reprotest/debian/copyright
+
+import pytest
+
+from reprotest.mdiffconf import *
+
+
+class LolX(collections.namedtuple("LolX", "x y z")):
+    pass
+
+def test_basic():
+    one = ImmutableNamespace(lol=LolX(x=strlist_set(";"), y=10, z=True), wtf=True)
+    zero = ImmutableNamespace(lol=LolX(x=strlist_set(";", ['less than nothing yo!']), y=0, z=False), wtf=False)
+    d = ImmutableNamespace()
+    d = parse_all(d, '+lol,wtf,-xxx,lol.x+=4;5;6,lol.y+=123,lol.x-=5,-lol.z', one, zero, sep=",")
+    assert d == ImmutableNamespace(lol=LolX(x=['4', '6'], y=133, z=False), wtf=True)
+    d = parse_all(d, '@lol,-lol.x,-lol.z,lol.x+=3;4;5', one, zero, sep=",")
+    assert d == ImmutableNamespace(lol=LolX(x=['less than nothing yo!', '3', '4', '5'], y=10, z=False), wtf=True)
diff --git a/tests/tests.py b/tests/test_reprotest.py
old mode 100755
new mode 100644
similarity index 94%
rename from tests/tests.py
rename to tests/test_reprotest.py
index 480b721..48984d4
--- a/tests/tests.py
+++ b/tests/test_reprotest.py
@@ -7,6 +7,7 @@ import sys
 
 import pytest
 import reprotest
+import reprotest.build
 
 REPROTEST = [sys.executable, "-m", "reprotest", "--no-diffoscope"]
 REPROTEST_TEST_SERVERS = os.getenv("REPROTEST_TEST_SERVERS", "null").split(",")
@@ -15,11 +16,12 @@ REPROTEST_TEST_DONTVARY = os.getenv("REPROTEST_TEST_DONTVARY", "").split(",")
 if REPROTEST_TEST_DONTVARY:
     REPROTEST += ["--dont-vary", ",".join(REPROTEST_TEST_DONTVARY)]
 
-TEST_VARIATIONS = frozenset(reprotest.VARIATIONS.keys()) - frozenset(REPROTEST_TEST_DONTVARY)
+TEST_VARIATIONS = frozenset(reprotest.build.VARIATIONS.keys()) - frozenset(REPROTEST_TEST_DONTVARY)
 
 def check_return_code(command, virtual_server, code):
     try:
-        retcode = reprotest.check(command, 'artifact', virtual_server, 'tests', variations=TEST_VARIATIONS)
+        retcode = reprotest.check(command, 'artifact', virtual_server, 'tests',
+            variations=reprotest.build.Variations.default(TEST_VARIATIONS))
     except SystemExit as system_exit:
         retcode = system_exit.args[0]
     finally:
@@ -59,7 +61,7 @@ def test_simple_builds(virtual_server):
     check_return_code('python3 mock_build.py irreproducible', virtual_server, 1)
 
 # TODO: test all variations that we support
- at pytest.mark.parametrize('captures', list(reprotest.VARIATIONS.keys()))
+ at pytest.mark.parametrize('captures', list(reprotest.build.VARIATIONS.keys()))
 def test_variations(virtual_server, captures):
     expected = 1 if captures in TEST_VARIATIONS else 0
     check_return_code('python3 mock_build.py ' + captures, virtual_server, expected)
diff --git a/tox.ini b/tox.ini
index feb0ba8..0362e07 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,5 +24,5 @@ deps =
 #  pytest-cov
   pytest
 # commands = py.test --cov-report html --cov=reprotest tests/tests.py
-commands = {envpython} -m coverage run --omit .tox/* --parallel -m py.test {posargs} tests/tests.py
+commands = {envpython} -m coverage run --omit .tox/* --parallel -m py.test {posargs} tests/
 # commands = py.test {posargs} tests/tests.py

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/reprotest.git



More information about the Reproducible-commits mailing list