]> err.no Git - dpkg/commitdiff
Add new module Dpkg::Deps to handle dependencies and some corresponding tests
authorRaphael Hertzog <hertzog@debian.org>
Sun, 14 Oct 2007 21:27:13 +0000 (23:27 +0200)
committerRaphael Hertzog <hertzog@debian.org>
Thu, 18 Oct 2007 12:30:16 +0000 (14:30 +0200)
The Dpkg::Deps::* modules allows various operations on dependencies such
as testing for implication, evaluating them given some packages known to be
installed, etc. They also allow the simplification of the dependencies.

ChangeLog
scripts/Dpkg/Deps.pm [new file with mode: 0644]
scripts/t/400_Dpkg_Deps.t [new file with mode: 0644]

index e2b63712828c17c46f45352fc53e204e019dd144..30fa32e5a765360bc7dab6481dab96f47ab77bbd 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -25,6 +25,9 @@
 2007-10-14  Raphael Hertzog  <hertzog@debian.org>
 
        * scripts/Dpkg/Version.pm: mark compare_versions as exportable.
+       * scripts/Dpkg/Deps.pm: new module to handle dependencies, to
+       evaluate them and check implications between them.
+       * scripts/t/400_Dpkg_Deps.t: non-regression tests for Dpkg::Deps.
 
 2007-10-13  Guillem Jover  <guillem@debian.org>
 
diff --git a/scripts/Dpkg/Deps.pm b/scripts/Dpkg/Deps.pm
new file mode 100644 (file)
index 0000000..cebbe9c
--- /dev/null
@@ -0,0 +1,784 @@
+# Copyright 2007 Raphael Hertzog <hertzog@debian.org>
+#
+# This program is free software; you may redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This is distributed in the hope that it will be useful, but without
+# any warranty; without even the implied warranty of merchantability or
+# fitness for a particular purpose. See the GNU General Public License
+# for more details.
+#
+# A copy of the GNU General Public License is available as
+# /usr/share/common-licenses/GPL in the Debian GNU/Linux distribution
+# or on the World Wide Web at http://www.gnu.org/copyleft/gpl.html.
+# You can also obtain it by writing to the Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+#########################################################################
+# Several parts are inspired by lib/Dep.pm from lintian (same license)
+#
+# Copyright (C) 1998 Richard Braakman
+# Portions Copyright (C) 1999 Darren Benham
+# Portions Copyright (C) 2000 Sean 'Shaleh' Perry
+# Portions Copyright (C) 2004 Frank Lichtenheld
+# Portions Copyright (C) 2006 Russ Allbery
+
+package Dpkg::Deps;
+
+use strict;
+use warnings;
+
+use Dpkg::Version qw(compare_versions);
+use Dpkg::Arch qw(get_host_arch);
+use Dpkg::ErrorHandling qw(warning);
+use Dpkg::Gettext;
+
+our @ISA = qw(Exporter);
+our @EXPORT_OK = qw(@pkg_dep_fields @src_dep_fields %dep_field_type);
+
+# Some generic variables
+our @pkg_dep_fields = qw(Pre-Depends Depends Recommends Suggests Enhances
+                         Conflicts Breaks Replaces Provides);
+our @src_dep_fields = qw(Build-Depends Build-Depends-Indep
+                         Build-Conflicts Build-Conflicts-Indep);
+our %dep_field_type = (
+       'Pre-Depends' => 'normal',
+       'Depends' => 'normal',
+       'Recommends' => 'normal',
+       'Suggests' => 'normal',
+       'Enhances' => 'union',
+       'Conflicts' => 'union',
+       'Breaks' => 'union',
+       'Replaces' => 'union',
+       'Provides' => 'union',
+       'Build-Depends' => 'normal',
+       'Build-Depends-Indep' => 'normal',
+       'Build-Conflicts' => 'union',
+       'Build-Conflicts-Indep' => 'union',
+);
+
+# Some factorized function
+
+# Returns true if the arch list $p is a superset of arch list $q
+sub arch_is_superset {
+    my ($p, $q) = @_;
+    my $p_arch_neg = defined($p) && $p->[0] =~ /^!/;
+    my $q_arch_neg = defined($q) && $q->[0] =~ /^!/;
+
+    # If "a" has no arches, it is a superset of q and we should fall through
+    # to the version check.
+    if (not defined $p) {
+       return 1;
+    }
+
+    # If q has no arches, it is a superset of p and there are no useful
+    # implications.
+    elsif (not defined $q) {
+       return 0;
+    }
+
+    # Both have arches.  If neither are negated, we know nothing useful
+    # unless q is a subset of p.
+    elsif (not $p_arch_neg and not $q_arch_neg) {
+       my %p_arches = map { $_ => 1 } @{$p};
+       my $subset = 1;
+       for my $arch (@{$q}) {
+           $subset = 0 unless $p_arches{$arch};
+       }
+       return 0 unless $subset;
+    }
+
+    # If both are negated, we know nothing useful unless p is a subset of
+    # q (and therefore has fewer things excluded, and therefore is more
+    # general).
+    elsif ($p_arch_neg and $q_arch_neg) {
+       my %q_arches = map { $_ => 1 } @{$q};
+       my $subset = 1;
+       for my $arch (@{$p}) {
+           $subset = 0 unless $q_arches{$arch};
+       }
+       return 0 unless $subset;
+    }
+
+    # If q is negated and p isn't, we'd need to know the full list of
+    # arches to know if there's any relationship, so bail.
+    elsif (not $p_arch_neg and $q_arch_neg) {
+       return 0;
+    }
+
+    # If p is negated and q isn't, q is a subset of p if none of the
+    # negated arches in p are present in q.
+    elsif ($p_arch_neg and not $q_arch_neg) {
+       my %q_arches = map { $_ => 1 } @{$q};
+       my $subset = 1;
+       for my $arch (@{$p}) {
+           $subset = 0 if $q_arches{substr($arch, 1)};
+       }
+       return 0 unless $subset;
+    }
+    return 1;
+}
+
+sub version_implies {
+    my ($rel_p, $v_p, $rel_q, $v_q) = @_;
+
+    # q wants an exact version, so p must provide that exact version.  p
+    # disproves q if q's version is outside the range enforced by p.
+    if ($rel_q eq '=') {
+        if ($rel_p eq '<<') {
+            return compare_versions($v_p, '<=', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '<=') {
+            return compare_versions($v_p, '<<', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '>>') {
+            return compare_versions($v_p, '>=', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '>=') {
+            return compare_versions($v_p, '>>', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '=') {
+            return compare_versions($v_p, '=', $v_q);
+        }
+    }
+
+    # A greater than clause may disprove a less than clause. An equal
+    # cause might as well.  Otherwise, if
+    # p's clause is <<, <=, or =, the version must be <= q's to imply q.
+    if ($rel_q eq '<=') {
+        if ($rel_p eq '>>') {
+            return compare_versions($v_p, '>=', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '>=') {
+            return compare_versions($v_p, '>>', $v_q) ? 0 : undef;
+       } elsif ($rel_p eq '=') {
+            return compare_versions($v_p, '<=', $v_q) ? 1 : 0;
+        } else { # <<, <=
+            return compare_versions($v_p, '<=', $v_q) ? 1 : undef;
+        }
+    }
+
+    # Similar, but << is stronger than <= so p's version must be << q's
+    # version if the p relation is <= or =.
+    if ($rel_q eq '<<') {
+        if ($rel_p eq '>>' or $rel_p eq '>=') {
+            return compare_versions($v_p, '>=', $v_p) ? 0 : undef;
+        } elsif ($rel_p eq '<<') {
+            return compare_versions($v_p, '<=', $v_q) ? 1 : undef;
+       } elsif ($rel_p eq '=') {
+            return compare_versions($v_p, '<<', $v_q) ? 1 : 0;
+        } else { # <<, <=
+            return compare_versions($v_p, '<<', $v_q) ? 1 : undef;
+        }
+    }
+
+    # Same logic as above, only inverted.
+    if ($rel_q eq '>=') {
+        if ($rel_p eq '<<') {
+            return compare_versions($v_p, '<=', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '<=') {
+            return compare_versions($v_p, '<<', $v_q) ? 0 : undef;
+       } elsif ($rel_p eq '=') {
+            return compare_versions($v_p, '>=', $v_q) ? 1 : 0;
+        } else { # >>, >=
+            return compare_versions($v_p, '>=', $v_q) ? 1 : undef;
+        }
+    }
+    if ($rel_q eq '>>') {
+        if ($rel_p eq '<<' or $rel_p eq '<=') {
+            return compare_versions($v_p, '<=', $v_q) ? 0 : undef;
+        } elsif ($rel_p eq '>>') {
+            return compare_versions($v_p, '>=', $v_q) ? 1 : undef;
+       } elsif ($rel_p eq '=') {
+            return compare_versions($v_p, '>>', $v_q) ? 1 : 0;
+        } else {
+            return compare_versions($v_p, '>>', $v_q) ? 1 : undef;
+        }
+    }
+
+    return undef;
+}
+
+sub parse {
+    my $dep_line = shift;
+    my %options = (@_);
+    $options{use_arch} = 1 if not exists $options{use_arch};
+    $options{reduce_arch} = 0 if not exists $options{reduce_arch};
+    $options{host_arch} = get_host_arch() if not exists $options{host_arch};
+    $options{union} = 0 if not exists $options{union};
+
+    my @dep_list;
+    foreach my $dep_and (split(/\s*,\s*/m, $dep_line)) {
+        my @or_list = ();
+        foreach my $dep_or (split(/\s*\|\s*/m, $dep_and)) {
+           my $dep_simple = Dpkg::Deps::Simple->new($dep_or);
+           if (not defined $dep_simple->{package}) {
+               warning(sprintf(_g("can't parse dependency %s"), $dep_and));
+               return undef;
+           }
+           $dep_simple->{arches} = undef if not $options{use_arch};
+            if ($options{reduce_arch}) {
+               $dep_simple->reduce_arch($options{host_arch});
+               next if not $dep_simple->arch_is_concerned($options{host_arch});
+           }
+           push @or_list, $dep_simple;
+        }
+       next if not @or_list;
+       if (scalar @or_list == 1) {
+           push @dep_list, $or_list[0];
+       } else {
+           my $dep_or = Dpkg::Deps::OR->new();
+           $dep_or->add($_) foreach (@or_list);
+           push @dep_list, $dep_or;
+       }
+    }
+    return undef if not @dep_list;
+    my $dep_and;
+    if ($options{union}) {
+       $dep_and = Dpkg::Deps::Union->new();
+    } else {
+       $dep_and = Dpkg::Deps::AND->new();
+    }
+    $dep_and->add($_) foreach (@dep_list);
+    return $dep_and;
+}
+
+package Dpkg::Deps::Simple;
+
+use strict;
+use warnings;
+
+use Dpkg::Arch qw(debarch_is);
+use Dpkg::Version qw(compare_versions);
+use Dpkg::ErrorHandling qw(internerr);
+use Dpkg::Gettext;
+
+sub new {
+    my ($this, $arg) = @_;
+    my $class = ref($this) || $this;
+    my $self = {
+       'package' => undef,
+       'relation' => undef,
+       'version' => undef,
+       'arches' => undef,
+    };
+    bless $self, $class;
+    $self->parse($arg) if defined($arg);
+    return $self;
+}
+
+sub parse {
+    my ($self, $dep) = @_;
+    return if not $dep =~
+            /^\s*                           # skip leading whitespace
+              ([a-zA-Z0-9][a-zA-Z0-9+.-]*)  # package name
+              (?:                           # start of optional part
+                \s* \(                      # open parenthesis for version part
+                \s* (<<|<=|=|>=|>>|<|>)     # relation part
+                \s* (.*?)                   # do not attempt to parse version
+                \s* \)                      # closing parenthesis
+              )?                            # end of optional part
+              (?:                           # start of optional architecture
+                \s* \[                      # open bracket for architecture
+                \s* (.*?)                   # don't parse architectures now
+                \s* \]                      # closing bracket
+              )?                            # end of optional architecture
+             \s*$                          # trailing spaces at end
+            /mx;
+    $self->{package} = $1;
+    $self->{relation} = $2;
+    $self->{version} = $3;
+    if (defined($4)) {
+       $self->{arches} = [ split(/\s+/, $4) ];
+    }
+    # Standardize relation field
+    if (defined($self->{relation})) {
+       $self->{relation} = '<<' if ($self->{relation} eq '<');
+       $self->{relation} = '>>' if ($self->{relation} eq '>');
+    }
+}
+
+sub dump {
+    my $self = shift;
+    my $res = $self->{package};
+    if (defined($self->{relation})) {
+       $res .= " (" . $self->{relation} . " " . $self->{version} .  ")";
+    }
+    if (defined($self->{'arches'})) {
+       $res .= " [" . join(" ", @{$self->{arches}}) . "]";
+    }
+    return $res;
+}
+
+# Returns true if the dependency in parameter can deduced from the current
+# dependency. Returns false if it can be negated. Returns undef if nothing
+# can be concluded.
+sub implies {
+    my ($self, $o) = @_;
+    if ($o->isa('Dpkg::Deps::Simple')) {
+       # An implication is only possible on the same package
+       return undef if $self->{package} ne $o->{package};
+
+       # Our architecture set must be a superset of the architectures for
+       # o, otherwise we can't conclude anything.
+       return undef unless Dpkg::Deps::arch_is_superset($self->{arches}, $o->{arches});
+
+       # If o has no version clause, then our dependency is stronger
+       return 1 if not defined $o->{relation};
+       # If o has a version clause, we must also have one, otherwise there
+       # can't be an implication
+       return undef if not defined $self->{relation};
+
+       return Dpkg::Deps::version_implies($self->{relation}, $self->{version},
+               $o->{relation}, $o->{version});
+
+    } elsif ($o->isa('Dpkg::Deps::AND')) {
+       # TRUE: Need to imply all individual elements
+       # FALSE: Need to NOT imply at least one individual element
+       my $res = 1;
+       foreach my $dep ($o->get_deps()) {
+           my $implication = $self->implies($dep);
+           unless (defined($implication) && $implication == 1) {
+               $res = $implication;
+               last if defined $res;
+           }
+       }
+       return $res;
+    } elsif ($o->isa('Dpkg::Deps::OR')) {
+       # TRUE: Need to imply at least one individual element
+       # FALSE: Need to not apply all individual elements
+       # UNDEF: The rest
+       my $res = undef;
+       foreach my $dep ($o->get_deps()) {
+           my $implication = $self->implies($dep);
+           if (defined($implication)) {
+               if (not defined $res) {
+                   $res = $implication;
+               } else {
+                   if ($implication) {
+                       $res = 1;
+                   } else {
+                       $res = 0;
+                   }
+               }
+               last if defined($res) && $res == 1;
+           }
+       }
+       return $res;
+    } else {
+       internerr(sprintf(_g("Dpkg::Deps::Simple can't evaluate implication with a %s!"), ref($o)));
+    }
+}
+
+sub get_deps {
+    my $self = shift;
+    return $self;
+}
+
+sub sort {
+    # Nothing to sort
+}
+
+sub arch_is_concerned {
+    my ($self, $host_arch) = @_;
+
+    return 0 if not defined $self->{package}; # Empty dep
+    return 1 if not defined $self->{arches};  # Dep without arch spec
+
+    my $seen_arch = 0;
+    foreach my $arch (@{$self->{arches}}) {
+       $arch=lc($arch);
+
+       if ($arch =~ /^!/) {
+           my $not_arch = $arch;
+           $not_arch =~ s/^!//;
+
+           if (debarch_is($host_arch, $not_arch)) {
+               $seen_arch = 0;
+               last;
+           } else {
+               # !arch includes by default all other arches
+               # unless they also appear in a !otherarch
+               $seen_arch = 1;
+           }
+       } elsif (debarch_is($host_arch, $arch)) {
+           $seen_arch = 1;
+           last;
+       }
+    }
+    return $seen_arch;
+}
+
+sub reduce_arch {
+    my ($self, $host_arch) = @_;
+    if (not $self->arch_is_concerned($host_arch)) {
+       $self->{package} = undef;
+       $self->{relation} = undef;
+       $self->{version} = undef;
+       $self->{arches} = undef;
+    } else {
+       $self->{arches} = undef;
+    }
+}
+
+sub get_evaluation {
+    my ($self, $facts) = @_;
+    return undef if not defined $self->{package};
+    my ($check, $param) = $facts->check_package($self->{package});
+    if ($check) {
+       if (defined $self->{relation}) {
+           if (ref($param)) {
+               # Provided packages
+               # XXX: Once support for versioned provides is in place,
+               # this part must be adapted
+               return 0;
+           } else {
+               if (defined($param)) {
+                   if (compare_versions($param, $self->{relation}, $self->{version})) {
+                       return 1;
+                   } else {
+                       return 0;
+                   }
+               } else {
+                   return undef;
+               }
+           }
+       } else {
+           return 1;
+       }
+    }
+    return undef;
+}
+
+sub simplify_deps {
+    my ($self, $facts) = @_;
+    my $eval = $self->get_evaluation($facts);
+    if (defined($eval) and $eval == 1) {
+       $self->{package} = undef;
+       $self->{relation} = undef;
+       $self->{version} = undef;
+       $self->{arches} = undef;
+    }
+}
+
+sub is_empty {
+    my $self = shift;
+    return not defined $self->{package};
+}
+
+sub merge_union {
+    my ($self, $o) = @_;
+    return 0 if not $o->isa('Dpkg::Deps::Simple');
+    return 0 if $self->is_empty() or $o->is_empty();
+    return 0 if $self->{package} ne $o->{package};
+    return 0 if defined $self->{arches} or defined $o->{arches};
+
+    if (not defined $o->{relation} and defined $self->{relation}) {
+       # Union is the non-versioned dependency
+       $self->{relation} = undef;
+       $self->{version} = undef;
+       return 1;
+    }
+
+    my $implication = $self->implies($o);
+    my $rev_implication = $o->implies($self);
+    if (defined($implication)) {
+       if ($implication) {
+           $self->{relation} = $o->{relation};
+           $self->{version} = $o->{version};
+           return 1;
+       } else {
+           return 0;
+       }
+    }
+    if (defined($rev_implication)) {
+       if ($rev_implication) {
+           # Already merged...
+           return 1;
+       } else {
+           return 0;
+       }
+    }
+    return 0;
+}
+
+package Dpkg::Deps::Multiple;
+
+use strict;
+use warnings;
+
+use Dpkg::ErrorHandling qw(internerr);
+
+sub new {
+    my $this = shift;
+    my $class = ref($this) || $this;
+    my $self = { 'list' => [ @_ ] };
+    bless $self, $class;
+    return $self;
+}
+
+sub add {
+    my $self = shift;
+    push @{$self->{list}}, @_;
+}
+
+sub get_deps {
+    my $self = shift;
+    return @{$self->{list}};
+}
+
+sub sort {
+    my $self = shift;
+    my @res = ();
+    @res = sort { $a->dump() cmp $b->dump() } @{$self->{list}};
+    $self->{list} = [ @res ];
+}
+
+sub arch_is_concerned {
+    my ($self, $host_arch) = @_;
+    my $res = 0;
+    foreach my $dep (@{$self->{list}}) {
+       $res = 1 if $dep->arch_is_concerned($host_arch);
+    }
+    return $res;
+}
+
+sub reduce_arch {
+    my ($self, $host_arch) = @_;
+    my @new;
+    foreach my $dep (@{$self->{list}}) {
+       $dep->reduce_arch($host_arch);
+       push @new, $dep if $dep->arch_is_concerned($host_arch);
+    }
+    $self->{list} = [ @new ];
+}
+
+sub is_empty {
+    my $self = shift;
+    return scalar @{$self->{list}} == 0;
+}
+
+sub merge_union {
+    internerr("The method merge_union() is only valid for Dpkg::Deps::Simple");
+}
+
+package Dpkg::Deps::AND;
+
+use strict;
+use warnings;
+
+our @ISA = qw(Dpkg::Deps::Multiple);
+
+sub dump {
+    my $self = shift;
+    return join(", ", map { $_->dump() } grep { not $_->is_empty() } $self->get_deps());
+}
+
+sub implies {
+    my ($self, $o) = @_;
+    # If any individual member can imply $o or NOT $o, we're fine
+    foreach my $dep ($self->get_deps()) {
+       my $implication = $dep->implies($o);
+       return 1 if defined($implication) && $implication == 1;
+       return 0 if defined($implication) && $implication == 0;
+    }
+    # If o is an AND, we might have an implication, if we find an
+    # implication within us for each predicate in o
+    if ($o->isa('Dpkg::Deps::AND')) {
+       my $subset = 1;
+       foreach my $odep ($o->get_deps()) {
+           my $found = 0;
+           foreach my $dep ($self->get_deps()) {
+               $found = 1 if $dep->implies($odep);
+           }
+           $subset = 0 if not $found;
+       }
+       return 1 if $subset;
+    }
+    return undef;
+}
+
+sub get_evaluation {
+    my ($self, $facts) = @_;
+    my $result = 1;
+    foreach my $dep ($self->get_deps()) {
+       my $eval = $dep->get_evaluation($facts);
+       if (not defined $eval) {
+           $result = undef;
+           last;
+       } elsif ($eval == 0) {
+           $result = 0;
+           last;
+       } elsif ($eval == 1) {
+           # Still possible
+       }
+    }
+    return $result;
+}
+
+sub simplify_deps {
+    my ($self, $facts, @knowndeps) = @_;
+    my @new;
+
+WHILELOOP:
+    while (@{$self->{list}}) {
+       my $dep = shift @{$self->{list}};
+       my $eval = $dep->get_evaluation($facts);
+       next if defined($eval) and $eval == 1;
+       foreach my $odep (@knowndeps, @new, @{$self->{list}}) {
+           next WHILELOOP if $odep->implies($dep);
+       }
+       push @new, $dep;
+    }
+    $self->{list} = [ @new ];
+}
+
+
+package Dpkg::Deps::OR;
+
+use strict;
+use warnings;
+
+our @ISA = qw(Dpkg::Deps::Multiple);
+
+sub dump {
+    my $self = shift;
+    return join(" | ", map { $_->dump() } grep { not $_->is_empty() } $self->get_deps());
+}
+
+sub implies {
+    my ($self, $o) = @_;
+
+    # Special case for AND with a single member, replace it by its member
+    if ($o->isa('Dpkg::Deps::AND')) {
+       my @subdeps = $o->get_deps();
+       if (scalar(@subdeps) == 1) {
+           $o = $subdeps[0];
+       }
+    }
+
+    # In general, an OR dependency can't imply anything except if each
+    # of its member implies a member in the other OR dependency
+    if ($o->isa('Dpkg::Deps::OR')) {
+       my $subset = 1;
+       foreach my $dep ($self->get_deps()) {
+           my $found = 0;
+           foreach my $odep ($o->get_deps()) {
+               $found = 1 if $dep->implies($odep);
+           }
+           $subset = 0 if not $found;
+       }
+       return 1 if $subset;
+    }
+    return undef;
+}
+
+sub get_evaluation {
+    my ($self, $facts) = @_;
+    my $result = 0;
+    foreach my $dep ($self->get_deps()) {
+       my $eval = $dep->get_evaluation($facts);
+       if (not defined $eval) {
+           $result = undef;
+           last;
+       } elsif ($eval == 1) {
+           $result = 1;
+           last;
+       } elsif ($eval == 0) {
+           # Still possible to have a false evaluation
+       }
+    }
+    return $result;
+}
+
+sub simplify_deps {
+    my ($self, $facts) = @_;
+    my @new;
+
+WHILELOOP:
+    while (@{$self->{list}}) {
+       my $dep = shift @{$self->{list}};
+       my $eval = $dep->get_evaluation($facts);
+       if (defined($eval) and $eval == 1) {
+           $self->{list} = [];
+           return;
+       }
+       foreach my $odep (@new, @{$self->{list}}) {
+           next WHILELOOP if $odep->implies($dep);
+       }
+       push @new, $dep;
+    }
+    $self->{list} = [ @new ];
+}
+
+package Dpkg::Deps::Union;
+
+use strict;
+use warnings;
+
+our @ISA = qw(Dpkg::Deps::Multiple);
+
+sub dump {
+    my $self = shift;
+    return join(", ", map { $_->dump() } grep { not $_->is_empty() } $self->get_deps());
+}
+
+sub implies {
+    # Implication test are not useful on Union
+    return undef;
+}
+
+sub get_evaluation {
+    # Evaluation are not useful on Union
+    return undef;
+}
+
+sub simplify_deps {
+    my ($self, $facts) = @_;
+    my @new;
+
+WHILELOOP:
+    while (@{$self->{list}}) {
+       my $dep = shift @{$self->{list}};
+       foreach my $odep (@new) {
+           next WHILELOOP if $dep->merge_union($odep);
+       }
+       push @new, $dep;
+    }
+    $self->{list} = [ @new ];
+}
+
+package Dpkg::Deps::KnownFacts;
+
+use strict;
+use warnings;
+
+sub new {
+    my $this = shift;
+    my $class = ref($this) || $this;
+    my $self = { 'pkg' => {}, 'virtualpkg' => {} };
+    bless $self, $class;
+    return $self;
+}
+
+sub add_installed_package {
+    my ($self, $pkg, $ver) = @_;
+    $self->{pkg}{$pkg} = $ver;
+}
+
+sub add_provided_package {
+    my ($self, $pkg, $rel, $ver, $by) = @_;
+    if (not exists $self->{virtualpkg}{$pkg}) {
+       $self->{virtualpkg}{$pkg} = [];
+    }
+    push @{$self->{virtualpkg}{$pkg}}, [ $by, $rel, $ver ];
+}
+
+sub check_package {
+    my ($self, $pkg) = @_;
+    if (exists $self->{pkg}{$pkg}) {
+       return (1, $self->{pkg}{$pkg});
+    }
+    if (exists $self->{virtualpkg}{$pkg}) {
+       return (1, $self->{virtualpkg}{$pkg});
+    }
+    return (0, undef);
+}
+
+1;
diff --git a/scripts/t/400_Dpkg_Deps.t b/scripts/t/400_Dpkg_Deps.t
new file mode 100644 (file)
index 0000000..82565ae
--- /dev/null
@@ -0,0 +1,56 @@
+# -*- mode: cperl;-*-
+
+use Test::More tests => 12;
+
+use strict;
+use warnings;
+
+use_ok('Dpkg::Deps');
+
+my $field_multiline = "libgtk2.0-common (= 2.10.13-1)  , libatk1.0-0 (>=
+1.13.2), libc6 (>= 2.5-5), libcairo2 (>= 1.4.0), libcupsys2 (>= 1.2.7),
+libfontconfig1 (>= 2.4.0), libglib2.0-0  (  >= 2.12.9), libgnutls13 (>=
+1.6.3-0), libjpeg62, python (<< 2.5)";
+my $field_multiline_sorted = "libatk1.0-0 (>= 1.13.2), libc6 (>= 2.5-5), libcairo2 (>= 1.4.0), libcupsys2 (>= 1.2.7), libfontconfig1 (>= 2.4.0), libglib2.0-0 (>= 2.12.9), libgnutls13 (>= 1.6.3-0), libgtk2.0-common (= 2.10.13-1), libjpeg62, python (<< 2.5)";
+
+my $dep_multiline = Dpkg::Deps::parse($field_multiline);
+$dep_multiline->sort();
+is($dep_multiline->dump(), $field_multiline_sorted, "Parse, sort and dump");
+
+my $dep_subset = Dpkg::Deps::parse("libatk1.0-0 (>> 1.10), libc6, libcairo2");
+is($dep_multiline->implies($dep_subset), 1, "Dep implies subset of itself");
+is($dep_subset->implies($dep_multiline), undef, "Subset doesn't imply superset");
+my $dep_opposite = Dpkg::Deps::parse("python (>= 2.5)");
+is($dep_opposite->implies($dep_multiline), 0, "Opposite condition implies NOT the depends");
+
+my $dep_or1 = Dpkg::Deps::parse("a|b (>=1.0)|c (>= 2.0)");
+my $dep_or2 = Dpkg::Deps::parse("x|y|a|b|c (<= 0.5)|c (>=1.5)|d|e");
+is($dep_or1->implies($dep_or2), 1, "Implication between OR 1/2");
+is($dep_or2->implies($dep_or1), undef, "Implication between OR 2/2");
+
+my $field_arch = "libc6 (>= 2.5) [!alpha !hurd-i386], libc6.1 [alpha], libc0.1 [hurd-i386]";
+my $dep_i386 = Dpkg::Deps::parse($field_arch, reduce_arch => 1, host_arch => 'i386');
+my $dep_alpha = Dpkg::Deps::parse($field_arch, reduce_arch => 1, host_arch => 'alpha');
+my $dep_hurd = Dpkg::Deps::parse($field_arch, reduce_arch => 1, host_arch => 'hurd-i386');
+is($dep_i386->dump(), "libc6 (>= 2.5)", "Arch reduce 1/3");
+is($dep_alpha->dump(), "libc6.1", "Arch reduce 2/3");
+is($dep_hurd->dump(), "libc0.1", "Arch reduce 3/3");
+
+
+my $facts = Dpkg::Deps::KnownFacts->new();
+$facts->add_installed_package("mypackage", "1.3.4-1");
+$facts->add_provided_package("myvirtual", undef, undef, "mypackage");
+
+my $field_duplicate = "libc6 (>= 2.3), libc6 (>= 2.6-1), mypackage (>=
+1.3), myvirtual | something, python (>= 2.5)";
+my $dep_dup = Dpkg::Deps::parse($field_duplicate);
+$dep_dup->simplify_deps($facts, $dep_opposite);
+is($dep_dup->dump(), "libc6 (>= 2.6-1)", "Simplify deps");
+
+my $field_dup_union = "libc6 (>> 2.3), libc6 (>= 2.6-1), fake (<< 2.0),
+fake(>> 3.0), fake (= 2.5), python (<< 2.5), python (= 2.4)";
+my $dep_dup_union = Dpkg::Deps::parse($field_dup_union, union => 1);
+$dep_dup_union->simplify_deps($facts);
+is($dep_dup_union->dump(), "libc6 (>> 2.3), fake (<< 2.0), fake (>> 3.0), fake (= 2.5), python (<< 2.5)", "Simplify union deps");
+
+