App-OpenHAP
view release on metacpan or search on metacpan
lib/App/OpenHAP/Test/Integration.pm view on Meta::CPAN
open my $fh, '<', PAIRINGS_FILE or return 0;
while (<$fh>) {
next if /^#/ || /^\s*$/;
close $fh;
return 1;
}
close $fh;
return 0;
}
# $self->_verify_unpaired():
# Probe the pairing state with POST /identify. The daemon
# returns 204 only when unpaired. It returns 400 with
# {"status":-70401} when still paired (HAP-HTTP.md §3). This
# sub fails loudly on the paired answer. It closes the probe
# socket immediately. The socket does not stay registered with
# the daemon until teardown.
sub _verify_unpaired ($self)
{
my $before = scalar @{ $self->{sockets} };
my $response = $self->http_request( 'POST', '/identify' );
for my $socket ( splice @{ $self->{sockets} }, $before ) {
$socket->close if defined $socket;
}
unless ( defined $response ) {
warn "No response to identify probe\n";
return;
}
my ($status) = parse_http_response($response);
return 1 if defined $status && $status == 204;
warn sprintf "Daemon not unpaired: identify returned %s\n",
$status // 'no status';
return;
}
# $self->close_sockets():
# Close and forget every raw socket that http_request opened.
# Then a probe connection does not stay registered with the
# daemon until teardown.
sub close_sockets ($self)
{
for my $socket ( @{ $self->{sockets} } ) {
$socket->close if defined $socket;
}
$self->{sockets} = [];
return 1;
}
sub http_request ( $self, $method, $path, $body = undef, $headers = {} )
{
my $socket = IO::Socket::INET->new(
PeerAddr => '127.0.0.1',
PeerPort => $self->{hap_port},
Proto => 'tcp',
Timeout => 2,
);
return unless defined $socket;
push @{ $self->{sockets} }, $socket;
print {$socket} Protocol::HAP::HTTP::build_request(
method => $method,
path => $path,
body => $body,
headers => { Host => "127.0.0.1:$self->{hap_port}", %$headers },
);
$socket->flush;
# Read until the buffer holds one whole message. A stream
# socket gives whatever arrived, which is not a message.
my $response = '';
while (1) {
last
if Protocol::HAP::HTTP::message_complete( $response,
max_size => MAX_RESPONSE );
my $bytes = $socket->sysread( my $chunk, 65536 );
last unless $bytes;
$response .= $chunk;
}
return $response;
}
sub parse_http_response ($response)
{
return unless defined $response;
my $parsed = Protocol::HAP::HTTP::parse_response($response) or return;
return ( $parsed->{status}, $parsed->{headers}, $parsed->{body} );
}
# status($response):
# Return the status code of an HTTP response, for the tests
# that need nothing else from it.
sub status ($response)
{
my ($status) = parse_http_response($response);
return $status;
}
# $self->find_char($database, $type, %opt):
# Find a characteristic by its short type string in a decoded
# /accessories structure. The walk skips the bridge (aid 1).
# The option name limits the walk to the accessory whose Name
# characteristic holds that value. The option ev demands the ev
# permission. Return (aid, iid, characteristic) on a match.
# Return the empty list when there is no match.
sub find_char ( $self, $database, $type, %opt )
{
for my $accessory ( @{ $database->{accessories} // [] } ) {
next if $accessory->{aid} == 1;
next
lib/App/OpenHAP/Test/Integration.pm view on Meta::CPAN
}
elsif ( defined $value && "$value" eq "$want" ) {
return 1;
}
return if time >= $deadline;
sleep 0.25;
}
}
sub get_config_value ( $self, $key )
{
return $self->{config}{$key};
}
# $self->get_device_topics():
# Return the MQTT topic of each configured device that has one.
sub get_device_topics ($self)
{
return
map { $_->{topic} } grep { defined $_->{topic} } $self->get_devices;
}
# $self->get_devices():
# Return the configured device records as a list of hashes with
# type, subtype, id, name, and topic.
sub get_devices ($self)
{
return @{ $self->{devices} // [] };
}
sub ensure_daemon_running ($self)
{
if ( !_rcctl( 'check', 'openhapd' ) ) {
_rcctl( 'start', 'openhapd' );
sleep 1;
return if !_rcctl( 'check', 'openhapd' );
}
# A daemon that runs per rcctl does not serve yet. At
# startup, the daemon publishes the mDNS advertisement and
# waits for the mdnsd replies. The HAP listener opens after
# that. Thus wait for the port, not for a fixed sleep.
return $self->wait_for_hap_port;
}
# $self->wait_for_hap_port($timeout):
# Wait until the HAP port accepts connections. Poll every
# quarter second, up to $timeout seconds. The default is 30
# seconds, which is generous for TCG emulation. Return 1 when
# the daemon serves. Return undef on the deadline.
sub wait_for_hap_port ( $self, $timeout = 30 )
{
my $port = $self->get_config_value('hap_port') // DEFAULT_HAP_PORT;
my $deadline = time + $timeout;
while ( time < $deadline ) {
my $socket = IO::Socket::INET->new(
PeerAddr => '127.0.0.1',
PeerPort => $port,
Proto => 'tcp',
Timeout => 2,
);
if ( defined $socket ) {
$socket->close;
return 1;
}
sleep 0.25;
}
return;
}
sub ensure_daemon_stopped ($self)
{
return 1 if !_rcctl( 'check', 'openhapd' );
_rcctl( 'stop', 'openhapd' );
sleep 1;
return !_rcctl( 'check', 'openhapd' );
}
# $self->restart_daemon():
# Restart openhapd through rcctl. Then wait until the HAP port
# serves again. Return 1 when the daemon serves. Return undef
# on failure.
sub restart_daemon ($self)
{
_rcctl( 'restart', 'openhapd' );
$self->wait_for_hap_port or return;
return 1;
}
# $self->ensure_mdnsd_running():
# Make sure that mdnsd runs and continues to run. Start it if
# necessary. Then check again across a settle window. A
# point-in-time probe races green when mdnsd starts and then
# exits shortly after. On failure, emit the captured
# diagnostics. Then a dead mdnsd is diagnosable from the test
# output, and the sub does not fail bare.
sub ensure_mdnsd_running ($self)
{
my $check = sub { _rcctl( 'check', 'mdnsd' ) };
unless ( $check->() ) {
_rcctl( 'enable', 'mdnsd' );
_rcctl( 'start', 'mdnsd' );
}
for my $probe ( 1 .. 3 ) {
sleep 1;
next if $check->();
$self->_warn_mdnsd_diagnostics(
"mdnsd not running at settle probe $probe/3");
return;
}
return 1;
}
( run in 0.888 second using v1.01-cache-2.11-cpan-85d3896f969 )