The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

# Copyright 2014 - present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
use strict;
use Test::More 0.96;
use Test::Deep qw/:v1/;
use Time::HiRes qw/time/;
use BSON::Types ':all';
use lib "t/lib";
my $iterator = path('t/data/SDAM')->iterator({recurse => 1});
my $cb = MongoDBTest::Callback->new;
while ( my $path = $iterator->() ) {
next unless -f $path && $path =~ /\.json$/;
my $plan = eval { decode_json( $path->slurp_utf8 ) };
if ( $@ ) {
die "Error decoding $path: $@";
}
run_test($path->relative('t/data/SDAM'), $plan);
}
sub create_mock_topology {
my ($name, $string) = @_;
my $uri = MongoDB::_URI->new( uri => $string );
my $seed_count = scalar @{ $uri->hostids };
# XXX this is a hack because the direct tests are written to
# assume Single, even though this is not implied by the spec
my $type = ( ($name =~ /^single|monitoring\/standalone/) && $seed_count == 1 )
? 'Single' : "Unknown";
return MongoDB::_Topology->new(
uri => $uri,
type => $type,
replica_set_name => $uri->options->{replicaset} || '',
min_server_version => "0.0.0",
max_wire_version => 2,
min_wire_version => 0,
credential => MongoDB::_Credential->new(
mechanism => 'NONE',
monitoring_callback => undef
),
monitoring_callback => $cb->callback,
);
}
sub run_test {
my ($name, $plan) = @_;
$name =~ s/\.json$//;
# TODO: Fix issue with PossiblePrimary and MongoDB::_Topology::_update_rs_without_primary
return if $name eq 'rs/primary_hint_from_secondary_with_mismatched_me';
subtest "$name" => sub {
my $events_arrayref;
if ( index($name,"monitoring/") != -1 ) {
my @events_array = @{ $plan->{'phases'}->[0]->{'outcome'}->{'events'} };
# Rearranging the test events because server_opening is out of order
my @server_opening_events;
for my $event_idx ( 0..$#events_array ) {
if ((keys %{$events_array[$event_idx]})[0] eq 'server_opening_event') {
push @server_opening_events, delete $events_array[$event_idx];
}
}
my @new_events = (map { defined($_) ? ($_) : () } @events_array);
my $topo_idx = -1;
for my $event_idx ( 0..$#new_events ) {
if ((keys %{$new_events[$event_idx]})[0] eq 'topology_description_changed_event') {
$topo_idx = $event_idx;
last;
}
}
@events_array = (
@new_events[0..$topo_idx-1],
@server_opening_events,
@new_events[$topo_idx..$#new_events],
);
$plan->{'phases'}->[0]->{'outcome'}->{'events'} = \@events_array;
$events_arrayref = \@events_array;
}
$cb->clear_events;
my $topology = create_mock_topology( $name, $plan->{'uri'} );
for my $phase (@{$plan->{'phases'}}) {
for my $response (@{$phase->{'responses'}}) {
my ($addr, $is_master) = @$response;
if ( defined $is_master->{'electionId'} ){
$is_master->{'electionId'} = bson_oid($is_master->{'electionId'}->{'$oid'});
}
# Process response
my $desc = MongoDB::_Server->new(
address => $addr,
is_master => $is_master,
last_update_time => time,
);
$topology->_update_topology_from_server_desc( @$response[0], $desc);
}
# Need to force this check for compatibility checking
# scan_all_servers wont work as there arent actually any servers...
$topology->_check_wire_versions;
# Check events if monitoring
if ($phase->{'outcome'}->{'events'}) {
check_outcome_event(
$name,
$cb->events,
$events_arrayref,
$topology)
}
# Process outcome
if ($phase->{'outcome'}->{'servers'}) {
check_outcome_servers($topology, $phase->{'outcome'}, $name);
}
}
};
}
sub check_outcome_event {
my ($name, $cb_events, $test_events, $topology) = @_;
# In these tests we pull out the callback and test events in order to check
# that they match
# topology_opening_event test
# 42 in $test_events topologyId is a dud, check only $cb_events one for existing
# By using values and brackets around declaration we grab the object so that
# We can avoid continually calling ->{event}->{thing} due to the nature of
# the event array given
my $topo_open_cb = shift @{$cb_events};
shift @{$test_events};
is($topo_open_cb->{'topologyId'}, "$topology",
"topology_open has topologyId that matches topology");
# server_opening_event test
# Note from hereon in that we use type and a different structure than the json
while ( (keys %{$test_events->[0]})[0] eq 'server_opening_event' ) {
my $server_cb = shift @{$cb_events};
my ($server_test) = values %{shift @{$test_events}};
is($server_cb->{'topologyId'}, "$topology",
"server_opening has topologyId that matches topology");
is(
$server_cb->{'address'},
$server_test->{'address'},
"server opening matches"
);
}
# topology_description_changed_event test now server(s) have opened
check_topology_description(
shift @{$cb_events},
values %{shift @{$test_events}},
$topology,
$name
);
# check server_description_changed_event for change in servers
my $desc_change_cb = shift @{$cb_events};
my ($desc_change_test) = values %{shift @{$test_events}};
is($desc_change_cb->{'topologyId'}, "$topology",
"server_description_changed has topologyId that matches topology");
is(
$desc_change_cb->{'address'},
$desc_change_test->{'address'},
"server_description_changed server address matches"
);
check_server_description(
$desc_change_cb->{'previousDescription'},
$desc_change_test->{'previousDescription'}
);
check_server_description(
$desc_change_cb->{'newDescription'},
$desc_change_test->{'newDescription'}
);
# server_closing_event test if any
while ( (keys %{$test_events->[0]})[0] eq 'server_closed_event') {
my $server_cb = shift @{$cb_events};
my ($server_test) = values %{shift @{$test_events}};
is($server_cb->{'topologyId'}, "$topology",
"has topologyId that matches topology");
is(
$server_cb->{'address'},
$server_test->{'address'},
"server closing matches"
);
}
# topology_description_changed_event test now server(s) have been changed
check_topology_description(
shift @{$cb_events},
values %{shift @{$test_events}},
$topology,
$name
);
}
sub check_topology_description {
my ($topo_desc_cb, $topo_desc_test, $topology, $name) = @_;
is($topo_desc_cb->{'topologyId'}, "$topology",
"topology_description_changed has topologyId that matches topology");
is(
scalar @{$topo_desc_cb->{previousDescription}->{'servers'}},
scalar @{$topo_desc_test->{previousDescription}->{'servers'}},
"topology_description_changed previousDescription correct amount of servers"
);
is(
scalar @{$topo_desc_cb->{newDescription}->{'servers'}},
scalar @{$topo_desc_test->{newDescription}->{'servers'}},
"topology_description_changed newDescription correct amount of servers"
);
# XXX hack that partially circumvents create_mock_topology hack
# because we do single too early
# This only applies to the initial topology opening event
if (
(index($name,"monitoring/standalone") != -1) &&
($topo_desc_test->{'previousDescription'}->{'topologyType'} eq "Unknown")
) {
$topo_desc_test->{'previousDescription'}->{'topologyType'} = "Single";
}
is(
$topo_desc_cb->{'previousDescription'}->{'topologyType'},
$topo_desc_test->{'previousDescription'}->{'topologyType'},
"previous topology description topologyType matches"
);
is(
$topo_desc_cb->{'newDescription'}->{'topologyType'},
$topo_desc_test->{'newDescription'}->{'topologyType'},
"new topology description topologyType matches"
);
# We insert replica_set_name (setName) on creation, earlier than the spec
is(
$topo_desc_cb->{'previousDescription'}->{'setName'},
$topo_desc_test->{'previousDescription'}->{'setName'},
"setName matches"
) if defined $topo_desc_test->{'previousDescription'}->{'setName'};
is(
$topo_desc_cb->{'newDescription'}->{'setName'},
$topo_desc_test->{'newDescription'}->{'setName'},
"setName matches"
) if defined $topo_desc_test->{'newDescription'}->{'setName'};
my @sorted_prev_servers = sort {$$a{"address"} cmp $$b{"address"} }
@{$topo_desc_cb->{'previousDescription'}->{'servers'}};
for my $i (0..$#{$topo_desc_cb->{'previousDescription'}->{'servers'}}) {
my $server_cb = $sorted_prev_servers[$i];
my $server_test = @{$topo_desc_test->{'previousDescription'}->{'servers'}}[$i];
check_server_description(
$server_cb,
$server_test
);
check_server_description(
$server_cb,
$server_test
);
}
my @sorted_new_servers = sort {$$a{"address"} cmp $$b{"address"} }
@{$topo_desc_cb->{'newDescription'}->{'servers'}};
for my $i (0..$#{$topo_desc_cb->{'newDescription'}->{'servers'}}) {
my $server_cb = $sorted_new_servers[$i];
my $server_test = @{$topo_desc_test->{'newDescription'}->{'servers'}}[$i];
check_server_description(
$server_cb,
$server_test
);
check_server_description(
$server_cb,
$server_test
);
}
}
sub check_server_description {
my ($server_cb, $server_test) = @_;
my $server_cmp = {
address => $server_test->{'address'},
arbiters => [],
hosts => $server_test->{'hosts'},
passives => [],
((exists $server_test->{'primary'}) ?
(primary => $server_test->{'primary'}) : ()),
((exists $server_test->{'setName'}) ?
(setName => $server_test->{'setName'}) : ()),
type => $server_test->{'type'}
};
cmp_deeply(
$server_cb,
superhashof($server_cmp),
"server matches in single server comparison"
);
}
sub check_outcome_servers {
my ($topology, $outcome, $name) = @_;
my %expected_servers = %{$outcome->{'servers'}};
my %actual_servers = %{$topology->servers};
is(
scalar keys %actual_servers,
scalar keys %expected_servers,
'correct amount of servers'
);
while (my ($key, $value) = each %expected_servers) {
if ( ok( (exists $actual_servers{$key}), "$key exists in outcome") ) {
my $actual_server = $actual_servers{$key};
is($actual_server->type, $value->{'type'}, 'correct server type');
my $expected_set_name = defined $value->{'setName'} ? $value->{'setName'} : "";
is(
$actual_server->set_name,
$expected_set_name,
'correct setName for server'
);
}
}
my $expected_set_name = defined $outcome->{'setName'} ? $outcome->{'setName'} : "";
is($topology->replica_set_name, $expected_set_name, 'correct setName for topology');
is($topology->type, $outcome->{'topologyType'}, 'correct topology type');
is(
$topology->logical_session_timeout_minutes, $outcome->{'logicalSessionTimeoutMinutes'},
'correct ls timeout'
);
if ( defined $outcome->{'compatible'} ) {
my $compatibility = $outcome->{'compatible'} ? 1 : 0;
# perl driver specifically supports older servers - this goes against
# spec but allows for support of legacy servers.
$compatibility = 1 if $name =~ /too_old/;
is($topology->is_compatible, $compatibility, 'compatibility correct');
}
}
done_testing;