The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use Test::More tests => 272;

use Graph::Directed;
use Graph::Undirected;
use Graph::Traversal::DFS;

my $g0 = Graph::Undirected->new;
my $g1 = Graph::Directed->new;
my $g2 = Graph::Undirected->new; # cyclic
my $g3 = Graph::Undirected->new; # unconnected
my $g4 = Graph::Directed->new;   # cyclic loop
my $g5 = Graph::Directed->new;   # cyclic
my $g6 = Graph::Directed->new;
my $g7 = Graph::Undirected->new; # empty
my $g8 = Graph::Undirected->new; # only vertices
my $g9 = Graph::Directed->new;
my $ga = Graph::Directed->new;

$g0->add_path(qw(a b c));
$g0->add_path(qw(a b d));
$g0->add_path(qw(a e f));

$g1->add_path(qw(a b c));
$g1->add_path(qw(a b d));
$g1->add_path(qw(a e f));

$g2->add_cycle(qw(a b c));

$g3->add_path(qw(a b c));
$g3->add_path(qw(d e f));

$g4->add_cycle(qw(a));

$g5->add_cycle(qw(a b c));

$g6->add_path(qw(a b c));
$g6->add_path(qw(d e f));

$g9->add_cycle(qw(a b c));
$g9->add_path(qw(b d e f));
$g9->add_edge(qw(d f));

$ga->add_cycle(qw(a b c));
$ga->add_path(qw(b d e f));
$ga->add_edge(qw(d f));

sub simple {
    my $g = shift;
    my @v = $g->vertices;
    is(@_, @v, "vertices");
    my %v; $v{$_} ++ for @_;
    # is(...,0) is 5.00504-incompatible
    ok(!scalar(grep { ($v{$_} || 0) != 1 } @v), "... once");
}

{
    my $t = Graph::Traversal::DFS->new($g0);

    is($t->unseen, $g0->vertices, "fresh traversal");
    is($t->seen,   0);
    is($t->seeing, 0);

    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g0, @t0);
    simple($g0, @t1);
    simple($g0, @t2);

    is($t->graph, $g0, "graph");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g0,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->postorder;

    simple($g1, @t0);
    simple($g1, @t1);
    simple($g1, @t2);

    is("@pre",  "a b c d e f", "pre");
    is("@post", "c d b f e a", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   6, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c d e f", "seen all");
    is("@{[$t->roots]}", "a", "roots");
    ok( $t->is_root('a') );
    ok(!$t->is_root('b') );
    ok(!$t->is_root('c') );
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g1,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1,
				       first_root     => 'b');
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g1, @t0);
    simple($g1, @t1);
    simple($g1, @t2);

    is("@pre",  "b c d a e f", "pre");
    is("@post", "c d b f e a", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   6, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c d e f", "seen all");
    is("@{[$t->roots]}",  "b a", "roots");
    ok( $t->is_root('a') );
    ok( $t->is_root('b') );
    ok(!$t->is_root('c') );
}

{
    my $t0 = Graph::Traversal::DFS->new($g0, next_alphabetic => 1);
    is($t0->next, "a",   "scalar next");
    $t0->terminate;
    is($t0->next, undef, "terminate");
    $t0->reset;
    is($t0->next, "a",   "after reset scalar next");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g2,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g2, @t0);
    simple($g2, @t1);
    simple($g2, @t2);

    is("@pre",  "a b c",       "pre");
    is("@post", "c b a",       "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   3, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c", "seen all");
    is("@{[$t->roots]}", "a", "roots");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g3,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g3, @t0);
    simple($g3, @t1);
    simple($g3, @t2);

    is("@pre",  "a b c d e f", "pre");
    is("@post", "c b a f e d", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   6, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c d e f", "seen all");
    is("@{[$t->roots]}", "a d", "roots");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g4,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1,
				       find_a_cycle => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    is("@pre",  "a",       "pre");
    is("@post", "a",       "post");
    is("@t0",   "a",       "t0");
    is("@t1",   "a",       "t1");
    is("@t2",   "a",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   1, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a", "seen all");
    is("@{[$t->roots]}", "a", "roots");
    is("@{$t->{state}->{a_cycle}}", "a", "cycle");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g5,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1,
				       find_a_cycle => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    is("@pre",  "a b c",   "pre");
    is("@post", "c b",     "post");
    is("@t0",   "a b c",   "t0");
    is("@t1",   "c b",     "t1");
    is("@t2",   "c b",     "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   3, "seen all");
    is($t->seeing, 1, "seeing one");
    is("@{[sort $t->seen]}", "a b c", "seen all");
    is("@{[$t->roots]}", "a", "roots");
    is("@{$t->{state}->{a_cycle}}", "b c a", "cycle");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g2,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_alphabetic => 1,
				       find_a_cycle => 1);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    is("@pre",  "a b c",   "pre");
    is("@post", "c b",     "post");
    is("@t0",   "a b c",   "t0");
    is("@t1",   "c b",     "t1");
    is("@t2",   "c b",     "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   3, "seen all");
    is($t->seeing, 1, "seeing one");
    is("@{[sort $t->seen]}", "a b c", "seen all");
    is("@{[$t->roots]}", "a", "roots");
    is("@{$t->{state}->{a_cycle}}", "b c a", "cycle");
}

{
    my $g = Graph::Undirected->new;
    $g->add_path(qw(a b c d e));
    $g->add_path(qw(b f g));
    $g->add_cycle(qw(c h i));
    my @c = $g->find_a_cycle(next_alphabetic => 1);
    is(@c, 3, "find_a_cycle");
    is("@c", "h i c", "find_a_cycle");
}

{
    my $g = Graph::Directed->new;
    my $h = Graph::Undirected->new;

    $g->add_path(qw(a b c d e));
    $g->add_path(qw(b f g));
    $g->add_path(qw(c h i));

    ok($g->is_dag, "is_dag true for dag");

    $h->add_path(qw(a b c d e));
    $h->add_path(qw(b f g));
    $h->add_path(qw(c h i));

    ok(!$h->is_dag, "is_dag false for undirected");

    my @t = $g->topological_sort(next_alphabetic => 1);

    is(@t, 9, "topological_sort");
    is("@t", "a b f g c h i d e", "topological_sort");

    ok($g->is_dag, "directed acyclic is dag");

    $g->add_path(qw(i c));

    ok(!$g->is_dag, "directed cyclic is not dag");
}

{
    my $g = Graph::Undirected->new;

    ok(!$g->is_dag, "undirected is not dag");

    eval '$g->topological_sort';
    like($@, qr/^Graph::topological_sort: expected directed acyclic graph, got undirected, /, "topological_sort not for undirected");

    my $d = Graph::Directed->new;

    $d->add_cycle(qw(a b));

    eval '$d->toposort';
    like($@, qr/^Graph::topological_sort: expected directed acyclic graph, got cyclic, /, "topological_sort not for cyclic");
}

{
    ok( $g0->is_connected, "is_connected");
    eval '$g1->is_connected';
    like($@,
	qr/Graph::is_connected: expected undirected graph, got directed, /,
	"directed cannot be tested for connectedness/");
    ok( $g1->is_weakly_connected, "... directed is weakly connected");
    ok( $g2->is_connected, "... cyclic undirected" );
    ok(!$g3->is_connected, "... undirected unconnected");
    eval '$g4->is_connected';
    like($@,
	qr/Graph::is_connected: expected undirected graph, got directed, /,
	"... cyclic loop");
    ok( $g4->is_weakly_connected, "... cyclic loop weakly connected");
    eval '$g5->is_connected';
    like( $@,
	 qr/Graph::is_connected: expected undirected graph, got directed, /,
	"... cyclic directed");
    ok( $g5->is_weakly_connected, "... cyclic directed weakly connected");
    eval '$g6->is_connected';
    like($@,
         qr/Graph::is_connected: expected undirected graph, got directed, /,
         "... directed unconnected");
    ok(!$g6->is_weakly_connected, "... directed unconnected is not weakly connected");
}

{
    my $t = Graph::Traversal::DFS->new($g7);

    is($t->unseen, $g7->vertices, "empty graph");
    is($t->seen,   0);
    is($t->seeing, 0);

    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g7, @t0);
    simple($g7, @t1);
    simple($g7, @t2);
}

{
    $g8->add_vertices(qw(a b c d));

    my $t = Graph::Traversal::DFS->new($g8);

    is($t->unseen, $g8->vertices, "only vertices");
    is($t->seen,   0);
    is($t->seeing, 0);

    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    simple($g8, @t0);
    simple($g8, @t1);
    simple($g8, @t2);
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g3,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       first_root => "a",
				       next_root  => undef);
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    is("@pre",  "a b c", "pre");
    is("@post", "c b a", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 3, "unseen half");
    is($t->seen,   3, "seen half");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c", "seen half");
    is("@{[$t->roots]}", "a", "roots");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g3,
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       start => "a");

    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->dfs;

    is("@pre",  "a b c", "pre");
    is("@post", "c b a", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 3, "unseen half");
    is($t->seen,   3, "seen half");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c", "seen half");
    is("@{[$t->roots]}", "a", "roots");
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g0,
				       pre_edge  => sub { push @pre,  $_[0], $_[1] },
				       post_edge => sub { push @post, $_[0], $_[1] },
				       next_alphabetic => 1);

    $t->dfs;

    is("@pre",  "a b b c b d a e e f", "pre");
    is("@post", "b c b d a b e f a e", "post");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   6, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c d e f", "seen all");
    is("@{[$t->roots]}", "a", "roots");
}

my $gb = Graph->new;

$gb->add_cycle(qw(a b c));
$gb->add_path(qw(a c));
$gb->add_path(qw(a d b));
my @gb;
my $tb =	
    Graph::Traversal::DFS->
    new($gb,
	next_alphabetic => 1,
	pre_edge      => sub { push @gb, "pre_edge @_[0,1]" },
	post_edge     => sub { push @gb, "post_edge @_[0,1]" },
	non_tree_edge => sub { push @gb, "non_tree_edge @_[0,1]" },
	back_edge     => sub { push @gb, "back_edge @_[0,1]" },
	down_edge     => sub { push @gb, "down_edge @_[0,1]" },
	cross_edge    => sub { push @gb, "cross_edge @_[0,1]" }
       );

$tb->dfs;

is($gb[ 0], "pre_edge a b",     "pre_edge");
is($gb[ 1], "pre_edge b c",     "pre_edge");
is($gb[ 2], "post_edge b c",    "post_edge");
is($gb[ 3], "non_tree_edge c a", "non_tree_edge");
is($gb[ 4], "back_edge c a",    "back_edge");
is($gb[ 5], "post_edge a b",    "post_edge");
is($gb[ 6], "pre_edge a d",     "pre_edge");
is($gb[ 7], "post_edge a d",    "post_edge");
is($gb[ 8], "non_tree_edge d b", "non_tree_edge");
is($gb[ 9], "cross_edge d b",   "cross_edge");
is($gb[10], "non_tree_edge a c", "non_tree_edge");
is($gb[11], "down_edge a c",    "down_edge");
is( @gb, 12 );

ok( $tb->tree->has_edge('a', 'b'), "tree edge");
ok( $tb->tree->has_edge('b', 'c'), "tree edge");
ok( $tb->tree->has_edge('a', 'd'), "tree edge");

ok(!$tb->tree->has_edge('c', 'a'), "non_tree edge");
ok(!$tb->tree->has_edge('d', 'b'), "non_tree edge");
ok(!$tb->tree->has_edge('a', 'c'), "non_tree edge");

is( $tb->tree, "a-b,a-d,b-c", "tree" );

is( $tb->preorder_by_vertex('a'), 0, "preorder of a" );
is( $tb->preorder_by_vertex('b'), 1, "preorder of b" );
is( $tb->preorder_by_vertex('c'), 2, "preorder of c" );
is( $tb->preorder_by_vertex('d'), 3, "preorder of d" );

is( $tb->vertex_by_preorder(0), 'a', "preorder of a" );
is( $tb->vertex_by_preorder(1), 'b', "preorder of b" );
is( $tb->vertex_by_preorder(2), 'c', "preorder of c" );
is( $tb->vertex_by_preorder(3), 'd', "preorder of d" );

is( $tb->postorder_by_vertex('a'), 3, "postorder of a" );
is( $tb->postorder_by_vertex('b'), 1, "postorder of b" );
is( $tb->postorder_by_vertex('c'), 0, "postorder of c" );
is( $tb->postorder_by_vertex('d'), 2, "postorder of d" );

is( $tb->vertex_by_postorder(3), 'a', "postorder of a" );
is( $tb->vertex_by_postorder(1), 'b', "postorder of b" );
is( $tb->vertex_by_postorder(0), 'c', "postorder of c" );
is( $tb->vertex_by_postorder(2), 'd', "postorder of d" );

my %pre = $tb->preorder_vertices();

is( $pre{'a'}, 0, "preorder of a" );
is( $pre{'b'}, 1, "preorder of b" );
is( $pre{'c'}, 2, "preorder of c" );
is( $pre{'d'}, 3, "preorder of d" );
is( keys %pre, 4 );

my %post = $tb->postorder_vertices();

is( $post{'a'}, 3, "postorder of a" );
is( $post{'b'}, 1, "postorder of b" );
is( $post{'c'}, 0, "postorder of c" );
is( $post{'d'}, 2, "postorder of d" );
is( keys %post, 4 );

my $gc = Graph->new(multiedged => 1);

$gc->add_path(qw(a b));
$gc->add_path(qw(a b));

my @gc;
my $tc =	
    Graph::Traversal::DFS->
    new($gc,
	next_alphabetic => 1,
	pre_edge      => sub { push @gc, "pre_edge @_[0,1]" },
	post_edge     => sub { push @gc, "post_edge @_[0,1]" },
	non_tree_edge => sub { push @gc, "non_tree_edge @_[0,1]" },
	back_edge     => sub { push @gc, "back_edge @_[0,1]" },
	down_edge     => sub { push @gc, "down_edge @_[0,1]" },
	cross_edge    => sub { push @gc, "cross_edge @_[0,1]" },
	seen_edge     => sub { push @gc, "seen_edge @_[0,1]" }
       );

$tc->dfs;

is( $gc[0], "pre_edge a b", "pre_edge" );
is( $gc[1], "post_edge a b", "post_edge" );
is( $gc[2], "seen_edge a b", "seen_edge" );
is( @gc, 3 );

my $gd = Graph->new;
$gd->add_edge(qw(0 1));
$gd->add_edge(qw(0 10));
$gd->add_edge(qw(0 9));
my @gd0;
my $td0 = Graph::Traversal::DFS->new($gd, next_numeric => 1, pre => sub { push @gd0, $_[0] });
$td0->dfs;
is( "@gd0", "0 1 9 10", "next_numeric" );
my @gd1;
my $td1 = Graph::Traversal::DFS->new($gd, next_alphabetic => 1, pre => sub { push @gd1, $_[0] });
$td1->dfs;
is( "@gd1", "0 1 10 9", "next_alphabetic" );

eval 'Graph::Traversal::DFS->new(next_alphabetic => 1)';
like($@, qr/Graph::Traversal: first argument is not a Graph/, "sane args");

eval 'Graph::Traversal::DFS->new($gd, next_alphazetic => 1)';
like($@, qr/Graph::Traversal: unknown attribute 'next_alphazetic'/, "zetic");

ok(!$td1->has_state('zot'), "has_state");

is($td1->get_state('zot'), undef, "get_state");

ok($td1->set_state('zot', 42), "set_state");

ok($td1->has_state('zot'), "has_state");

is($td1->get_state('zot'), 42, "get_state");

ok($td1->delete_state('zot'), "delete_state");

ok(!$td1->has_state('zot'), "has_state");

is($td1->get_state('zot'), undef, "get_state");

{
    # http://rt.cpan.org/NoAuth/Bug.html?id=4420
    use Graph::Directed;
    my $g = new Graph::Directed;
    ok($g = $g->add_edge('a','b'), "rt.cpan.org 4420");
    ok($g->has_edge('a','b'));
    ok($g = $g->add_edge('b','a'));
    ok($g->has_edge('b','a'));
    my @toposort;
    eval '@toposort = $g->toposort';
    like($@, qr/Graph::topological_sort: expected directed acyclic graph, got cyclic/);
    # http://rt.cpan.org/NoAuth/Bug.html?id=5168
    @toposort = $g->toposort(empty_if_cyclic => 1);
    is(@toposort, 0, "rt.cpan.org 5168");
    # http://rt.cpan.org/NoAuth/Bug.html?id=5167
    ok( $g->has_a_cycle, "rt.cpan.org 5167" );
    my $h = Graph->new;
    $h->add_edge(qw(a b));
    $h->add_edge(qw(a c));
    ok(!$h->has_a_cycle);
}

{
    my @pre;
    my @post;
    my $t = Graph::Traversal::DFS->new($g0,
					   first_root => 'a',
				       pre  => sub { push @pre,  $_[0] },
				       post => sub { push @post, $_[0] },
				       next_successor => sub { shift; (reverse sort keys %{ $_[0] })[0] });
    my @t0 = $t->preorder;
    my @t1 = $t->postorder;
    my @t2 = $t->postorder;

    simple($g1, @t0);
    simple($g1, @t1);
    simple($g1, @t2);

    is("@pre",  "a e f b d c", "pre");
    is("@post", "f e d c b a", "post");
    is("@t0",   "@pre",        "t0");
    is("@t1",   "@post",       "t1");
    is("@t2",   "@post",       "t2");

    is($t->unseen, 0, "unseen none");
    is($t->seen,   6, "seen all");
    is($t->seeing, 0, "seeing none");
    is("@{[sort $t->seen]}", "a b c d e f", "seen all");
    is("@{[$t->roots]}", "a", "roots");
    ok( $t->is_root('a') );
    ok(!$t->is_root('b') );
    ok(!$t->is_root('c') );
}