Add tests for EZO::USB and MQTT modules.

Commit
1abfa90edfa41783eccd05969c65b3ddc867e34c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
roles/daq-node/t/01-ezo-usb.t
index 00000000..dbeff042 000000..100644
@@ -0,0 +1,106 @@
1 Added: # -*- mode: cperl; -*-
2 Added:
3 Added: use v5.40.1;
4 Added: use strict;
5 Added: use warnings;
6 Added:
7 Added: use Test2::V0;
8 Added:
9 Added: use FindBin;
10 Added: use lib "${FindBin::Bin}/../lib/perl5/";
11 Added:
12 Added: use FAPG::DAQ::EZO::USB qw(
13 Added: ezo_command
14 Added: read_probe_info
15 Added: detect_probe_type
16 Added: read_once
17 Added: parse_reading
18 Added: normalize_probe_type
19 Added: unit_for_probe_type
20 Added: );
21 Added:
22 Added: {
23 Added:
24 Added: package Local::FakeSerial;
25 Added:
26 Added: use v5.40.1;
27 Added: use warnings;
28 Added:
29 Added: sub new ( $class, @responses ) {
30 Added: return bless {
31 Added: responses => [ map { $_ =~ /\r\z/ ? $_ : "$_\r" } @responses ],
32 Added: rx => q{},
33 Added: writes => [],
34 Added: }, $class;
35 Added: }
36 Added:
37 Added: sub write ( $self, $bytes ) {
38 Added: push $self->{writes}->@*, $bytes;
39 Added: $self->{rx} .= shift( $self->{responses}->@* ) // q{};
40 Added: return length $bytes;
41 Added: }
42 Added:
43 Added: sub read ( $self, $wanted ) {
44 Added: return ( 0, q{} ) if $self->{rx} eq q{};
45 Added:
46 Added: my $chunk = substr $self->{rx}, 0, $wanted, q{};
47 Added: return ( length($chunk), $chunk );
48 Added: }
49 Added:
50 Added: sub writes ($self) {
51 Added: return $self->{writes};
52 Added: }
53 Added: }
54 Added:
55 Added: subtest 'basic command framing' => sub {
56 Added: my $serial = Local::FakeSerial->new('7.12');
57 Added:
58 Added: is ezo_command( $serial, 'R' ), '7.12', 'returns response without CR';
59 Added: is $serial->writes, ["R\r"], 'writes command with CR terminator';
60 Added: };
61 Added:
62 Added: subtest 'probe detection from info command' => sub {
63 Added: my $serial = Local::FakeSerial->new('?I,pH,2.15');
64 Added:
65 Added: my $info = read_probe_info($serial);
66 Added:
67 Added: is $info->{raw}, '?I,pH,2.15';
68 Added: is $info->{device}, 'pH';
69 Added: is $info->{firmware}, '2.15';
70 Added: is $info->{probe}, 'ph';
71 Added: is $info->{unit}, 'pH';
72 Added:
73 Added: is detect_probe_type( Local::FakeSerial->new('?I,ORP,2.13') ), 'redox';
74 Added: };
75 Added:
76 Added: subtest 'single-value reading' => sub {
77 Added: my $serial = Local::FakeSerial->new('8.34');
78 Added:
79 Added: my $reading = read_once( $serial, probe => 'DO' );
80 Added:
81 Added: is $reading->{probe}, 'do';
82 Added: is $reading->{unit}, 'mg/L';
83 Added: is $reading->{raw}, '8.34';
84 Added: is $reading->{value}, 8.34;
85 Added: is $reading->{values}, [8.34];
86 Added: };
87 Added:
88 Added: subtest 'EC multi-value reading keeps all values but uses first as primary' => sub {
89 Added: my $parsed = parse_reading( '1413,704,0.70,1.001', probe => 'ec' );
90 Added:
91 Added: is $parsed->{value}, 1413;
92 Added: is $parsed->{values}, [ 1413, 704, 0.70, 1.001 ];
93 Added: };
94 Added:
95 Added: subtest 'normalization and units' => sub {
96 Added: is normalize_probe_type('pH'), 'ph';
97 Added: is normalize_probe_type('Dissolved Oxygen'), 'do';
98 Added: is normalize_probe_type('ORP'), 'redox';
99 Added: is normalize_probe_type('EC'), 'ec';
100 Added:
101 Added: is unit_for_probe_type('ph'), 'pH';
102 Added: is unit_for_probe_type('redox'), 'mV';
103 Added: is unit_for_probe_type('ec'), 'uS/cm';
104 Added: };
105 Added:
106 Added: done_testing;
roles/daq-node/t/01-probe.t
index 3310f30c..00000000 100644..000000
@@ -1,89 +0,0 @@
1 Removed: #!/usr/bin/env perl
2 Removed: # -*- mode: cperl; -*-
3 Removed:
4 Removed: # Basic Atlas Scientific EZO probe smoke test.
5 Removed:
6 Removed: use 5.40.1;
7 Removed: use strict;
8 Removed: use warnings;
9 Removed:
10 Removed: use Test2::V0;
11 Removed: plan tests => 3;
12 Removed: use Device::SerialPort;
13 Removed: use Time::HiRes qw(usleep);
14 Removed:
15 Removed: my $port = '/dev/ttyUSB0';
16 Removed: my $baud = 9600;
17 Removed:
18 Removed: my $serial = Device::SerialPort->new($port)
19 Removed: or BAIL_OUT("Cannot open serial port: $port");
20 Removed:
21 Removed: $serial->baudrate($baud);
22 Removed: $serial->databits(8);
23 Removed: $serial->parity('none');
24 Removed: $serial->stopbits(1);
25 Removed:
26 Removed: $serial->read_char_time(0);
27 Removed: $serial->read_const_time(100);
28 Removed:
29 Removed: note("Testing EZO probe on $port at $baud baud");
30 Removed:
31 Removed: my $info = ezo_command($serial, 'I', 500_000);
32 Removed: note("I => " . printable($info));
33 Removed:
34 Removed: ok($info ne '', 'probe is reachable');
35 Removed: like($info, qr/(?:\?I,|EZO|PH|EC|DO|ORP)/i, 'probe is identifiable');
36 Removed:
37 Removed: my $reading = ezo_command($serial, 'R', 1_500_000);
38 Removed: note("R => " . printable($reading));
39 Removed:
40 Removed: like(
41 Removed: $reading,
42 Removed: qr/OK/,
43 Removed: 'probe returns a single reading'
44 Removed: );
45 Removed:
46 Removed: sub ezo_command {
47 Removed: my ($serial, $command, $wait_us) = @_;
48 Removed:
49 Removed: drain_serial($serial);
50 Removed:
51 Removed: my $written = $serial->write("$command\r");
52 Removed: return '' unless defined $written && $written > 0;
53 Removed:
54 Removed: usleep($wait_us);
55 Removed:
56 Removed: my $reply = '';
57 Removed: while (1) {
58 Removed: my ($count, $buffer) = $serial->read(255);
59 Removed: last unless $count;
60 Removed: $reply .= $buffer;
61 Removed: }
62 Removed:
63 Removed: return clean_reply($reply);
64 Removed: }
65 Removed:
66 Removed: sub drain_serial {
67 Removed: my ($serial) = @_;
68 Removed:
69 Removed: while (1) {
70 Removed: my ($count, undef) = $serial->read(255);
71 Removed: last unless $count;
72 Removed: }
73 Removed: }
74 Removed:
75 Removed: sub clean_reply {
76 Removed: my ($reply) = @_;
77 Removed:
78 Removed: $reply
79 Removed: =~ s/\r/\n/gr
80 Removed: =~ s/\n+/\n/gr
81 Removed: =~ s/^\n|\n$//g;
82 Removed:
83 Removed: return $reply;
84 Removed: }
85 Removed:
86 Removed: sub printable {
87 Removed: my ($value) = @_;
88 Removed: return $value eq '' ? '<no response>' : $value;
89 Removed: }
roles/daq-node/t/02-probe.t
index 00000000..f7108283 000000..100644
@@ -0,0 +1,84 @@
1 Added: #!/usr/bin/env perl
2 Added: # -*- mode: cperl; -*-
3 Added:
4 Added: # Basic Atlas Scientific EZO probe smoke test.
5 Added:
6 Added: use 5.40.1;
7 Added: use strict;
8 Added: use warnings;
9 Added:
10 Added: use Test2::V0;
11 Added: plan tests => 3;
12 Added: use Device::SerialPort;
13 Added: use Time::HiRes qw(usleep);
14 Added:
15 Added: my $port = '/dev/ttyUSB0';
16 Added: my $baud = 9600;
17 Added:
18 Added: my $serial = Device::SerialPort->new($port)
19 Added: or die "Cannot open serial port: $port";
20 Added:
21 Added: $serial->baudrate($baud);
22 Added: $serial->databits(8);
23 Added: $serial->parity('none');
24 Added: $serial->stopbits(1);
25 Added:
26 Added: $serial->read_char_time(0);
27 Added: $serial->read_const_time(100);
28 Added:
29 Added: note("Testing EZO probe on $port at $baud baud");
30 Added:
31 Added: my $info = ezo_command( $serial, 'I', 500_000 );
32 Added: note( "I => " . printable($info) );
33 Added:
34 Added: ok( $info ne '', 'probe is reachable' );
35 Added: like( $info, qr/(?:\?I,|EZO|PH|EC|DO|ORP)/i, 'probe is identifiable' );
36 Added:
37 Added: my $reading = ezo_command( $serial, 'R', 1_500_000 );
38 Added: note( "R => " . printable($reading) );
39 Added:
40 Added: like( $reading, qr/OK/, 'probe returns a single reading' );
41 Added:
42 Added: sub ezo_command {
43 Added: my ( $serial, $command, $wait_us ) = @_;
44 Added:
45 Added: drain_serial($serial);
46 Added:
47 Added: my $written = $serial->write("$command\r");
48 Added: return '' unless defined $written && $written > 0;
49 Added:
50 Added: usleep($wait_us);
51 Added:
52 Added: my $reply = '';
53 Added: while (1) {
54 Added: my ( $count, $buffer ) = $serial->read(255);
55 Added: last unless $count;
56 Added: $reply .= $buffer;
57 Added: }
58 Added:
59 Added: return clean_reply($reply);
60 Added: }
61 Added:
62 Added: sub drain_serial {
63 Added: my ($serial) = @_;
64 Added:
65 Added: while (1) {
66 Added: my ( $count, undef ) = $serial->read(255);
67 Added: last unless $count;
68 Added: }
69 Added: }
70 Added:
71 Added: sub clean_reply {
72 Added: my ($reply) = @_;
73 Added:
74 Added: $reply =~ s/\r/\n/g;
75 Added: $reply =~ s/\n+/\n/g;
76 Added: $reply =~ s/^\n|\n$//g;
77 Added:
78 Added: return $reply;
79 Added: }
80 Added:
81 Added: sub printable {
82 Added: my ($value) = @_;
83 Added: return $value eq '' ? '<no response>' : $value;
84 Added: }
roles/daq-node/t/03-mqtt.t
index 00000000..32a11110 000000..100644
@@ -0,0 +1,105 @@
1 Added: # -*- mode: cperl; -*-
2 Added:
3 Added: use v5.40.1;
4 Added: use strict;
5 Added: use warnings;
6 Added:
7 Added: use Test2::V0;
8 Added: use JSON::PP qw(decode_json);
9 Added:
10 Added: use FindBin;
11 Added: use lib "${FindBin::Bin}/../lib/perl5/";
12 Added:
13 Added: use FAPG::DAQ::MQTT qw(
14 Added: publish_mqtt
15 Added: mqtt_payload_for_reading
16 Added: mqtt_topic_for_reading
17 Added: mqtt_broker_endpoint
18 Added: );
19 Added:
20 Added: {
21 Added: package Local::FakeMQTT;
22 Added:
23 Added: use v5.40.1;
24 Added: use warnings;
25 Added:
26 Added: sub new ($class) {
27 Added: return bless { published => [] }, $class;
28 Added: }
29 Added:
30 Added: sub publish ($self, $topic, $payload) {
31 Added: push $self->{published}->@*, [$topic, $payload];
32 Added: return 1;
33 Added: }
34 Added:
35 Added: sub published ($self) {
36 Added: return $self->{published};
37 Added: }
38 Added: }
39 Added:
40 Added: subtest 'topic shape' => sub {
41 Added: my $topic = mqtt_topic_for_reading(
42 Added: { probe => 'pH', value => 7.12, unit => 'pH' },
43 Added: node => 'fapg-daq-zero-01',
44 Added: );
45 Added:
46 Added: is $topic, 'fapg/daq/ph/fapg-daq-zero-01/reading';
47 Added: };
48 Added:
49 Added: subtest 'payload shape' => sub {
50 Added: my $payload = mqtt_payload_for_reading(
51 Added: {
52 Added: probe => 'pH',
53 Added: value => 7.12,
54 Added: unit => 'pH',
55 Added: raw => '7.12',
56 Added: timestamp => '2026-07-06T10:00:00Z',
57 Added: },
58 Added: node => 'fapg-daq-zero-01',
59 Added: );
60 Added:
61 Added: my $decoded = decode_json($payload);
62 Added:
63 Added: is $decoded->{schema}, 'fapg.daq.reading.v1';
64 Added: is $decoded->{timestamp}, '2026-07-06T10:00:00Z';
65 Added: is $decoded->{node}, 'fapg-daq-zero-01';
66 Added: is $decoded->{probe}, 'ph';
67 Added: is $decoded->{value}, 7.12;
68 Added: is $decoded->{unit}, 'pH';
69 Added: is $decoded->{raw}, '7.12';
70 Added: is $decoded->{source}, 'fapg-daq-node';
71 Added: };
72 Added:
73 Added: subtest 'publish uses injected client, so unit tests need no broker' => sub {
74 Added: my $fake = Local::FakeMQTT->new;
75 Added:
76 Added: my $result = publish_mqtt(
77 Added: {
78 Added: probe => 'DO',
79 Added: value => 8.34,
80 Added: unit => 'mg/L',
81 Added: timestamp => '2026-07-06T10:00:00Z',
82 Added: },
83 Added: node => 'fapg-daq-zero-02',
84 Added: client => $fake,
85 Added: );
86 Added:
87 Added: is $result->{topic}, 'fapg/daq/do/fapg-daq-zero-02/reading';
88 Added: is scalar $fake->published->@*, 1;
89 Added: is $fake->published->[0][0], 'fapg/daq/do/fapg-daq-zero-02/reading';
90 Added:
91 Added: my $decoded = decode_json($fake->published->[0][1]);
92 Added: is $decoded->{probe}, 'do';
93 Added: is $decoded->{value}, 8.34;
94 Added: };
95 Added:
96 Added: subtest 'broker endpoint defaults to the Pi 5 broker' => sub {
97 Added: local $ENV{FAPG_MQTT_HOST};
98 Added: local $ENV{MQTT_HOST};
99 Added: local $ENV{FAPG_MQTT_PORT};
100 Added: local $ENV{MQTT_PORT};
101 Added:
102 Added: is mqtt_broker_endpoint(), 'fapg-daq-five-01:1883';
103 Added: };
104 Added:
105 Added: done_testing;