Automatisches Testen von Fhem-Modulen

Begonnen von StefanStrobel, 10 Januar 2021, 15:26:07

Vorheriges Thema - Nächstes Thema

StefanStrobel

Hallo,

ich habe für 98_HTTPMOD und 98_Modbus einige Tests geschrieben und eingecheckt, bei denen ich häufig in mehreren Schritten eine Netzwerk-Kommunikation und dazwischen Zeit für die Fhem-Hauptschleife benötige, damit die ReadFn etc. die Daten auch abarbeiten können. Nachdem ich das zunächst mit einer manuellen Kaskade on internalTimer-Aufrufen gemacht habe, habe ich es jetzt mit ein par Test-Utilities (lib/FHEM/Modbus/TestUtils.pm) vereinfacht. Vielleicht hat ja jemand das gleiche Problem und kann das brauchen.

Bei Modbus sieht der Test für eine Kommunikation von einem Master über ein Relay zu einem Slave z.B. so aus:


############################################################
# 54_RelayProto.cfg
############################################################
define D1 dummy
define Slave ModbusAttr 50 slave global:5501 ASCII
define RM ModbusAttr 50 0 localhost:5501 ASCII
define Relay ModbusAttr 5 relay localhost:5510 RTU to RM
define Master ModbusAttr 5 0 localhost:5510 RTU

attr Slave obj-h256-reading TempWasserEin
attr Slave obj-h258-reading D1:TempWasserAus

attr Slave obj-h100-reading Test1
attr Slave obj-h100-setexpr $val * 4
...



############################################################
# 54_RelayProto.t: test relay with different protocols
############################################################

package main;
use strict;
use warnings;
use Test::More;
use Time::HiRes     qw( gettimeofday tv_interval);  # return time as float, not just full seconds
use FHEM::HTTPMOD::Utils qw(:all);
use FHEM::Modbus::TestUtils qw(:all);

NextStep();             # start with the first testStep function and then automatically give some time to Fhem before the next step

sub testStep1 {         # preparation of slave content, enable devices
    LogStep "enable Master and set value at Slave";
    fhem ('attr RM disable 0');
    fhem ('attr Relay disable 0');
    fhem ('attr Master disable 0');
    fhem ('setreading Slave TempWasserEin 12');
    fhem ('setreading Slave Test1 1');
    fhem ('setreading Slave Test2 2.123');
    fhem ('setreading Slave Test3 abcdefg');
    fhem ('setreading Slave Test4 40');
    readingsSingleUpdate($defs{'Slave'}, 'Test5', pack('H*', 'e4f6fc'), 0);   

    fhem ('setreading Slave c0 1');
    fhem ('setreading Slave c5 1');
    fhem ('setreading Slave c17 1');
    return 0.1;         # ask for an internalTimer to call the next step after 0.1 seconds.
}

sub testStep2 {         # get holding registers
    LogStep "get TempWasserEin";
    fhem ('attr Master verbose 3');
    fhem ('attr Slave verbose 3');
    fhem ('attr Relay verbose 3');
    fhem ('attr RM verbose 3');
    fhem ('get Master TempWasserEin');
    fhem ('get Master Test1');
    fhem ('get Master Test2');
    fhem ('get Master Test3');
    fhem ('get Master Test4');
    fhem ('get Master Test5');
    return 1;
}

sub testStep3 {     # check results
    LogStep "check result";
    is(FhemTestUtils_gotEvent(qr/Master:TempWasserEin:\s12/xms), 1, "Retrieve integer value from local slave");
    is(FhemTestUtils_gotEvent(qr/Master:Test1: 6/), 1, "Retrieve another integer value with expressions on both sides from local slave");
    is(FhemTestUtils_gotEvent(qr/Master:Test2: 2.12/), 1, "Retrieve float value from local slave");
    is(FhemTestUtils_gotEvent(qr/Master:Test3: abcdefg/), 1, "Retrieve ascii value from local slave");
    is(FhemTestUtils_gotEvent(qr/Master:Test4: 40/), 0, "ignoreExpr prohibits Test4 set to 40");
    is(FhemTestUtils_gotEvent(qr/Master:Test5: äöü/), 1, "encode worked for Test5");
    return;
}
usw.


So muss ich nicht in jeder einzelnen Funktion extra loggen, welcher Schritt jetzt ausgeführt wird, wieder einen internen Timer setzen etc.

Zudem habe ich ein paar Funktionen hinzugefügt, um das Empfangen einer Antwort simulieren zu können.
Das verwende ich z.B. um die richtige Funktion der konfigurierten Delays bei Modbus zu testen:


##############################################
# test communication delays
##############################################
package main;
use strict;
use warnings;
use Test::More;
use Time::HiRes     qw( gettimeofday tv_interval);  # return time as float, not just full seconds
use FHEM::HTTPMOD::Utils qw(:all);
use FHEM::Modbus::TestUtils qw(:all);

my %rData = (
'010403d1000221b6' => '01040400000000fb84',
'010404c100022107' => '010404000041b4cba3',
'0104060100022083' => '0104045262419dbb1b',
'01040691000220ae' => '010404000042524ad9',
'01040a4100022207' => '010404533c458c19',
'01040a61000223cd' => '01040400000000fb84',
'01040a810002223b' => '0104049dff454cd77d',
'01040aa1000223f1' => '01040400000000fb84',
'01040ac1000223ef' => '0104044d6345282e78',
'01040ae100022225' => '010404b6f644980f54',
'010411d1000224ce' => '01040400000000fb84',

'050303020001240a' => '0503020122c80d',
'05030309000155c8' => '05030200004984',
'0503010600016473' => '0503020106c816',
'05030100000585b1' => '05030a0137110001381100010dac7b',
'0503010000018472' => '050302013709c2',

'0506030900005808' => '0506030900005808',                   # set hyst mode
'0506030201182850' => '0506030201182850'                    # set temp soll 28
);

SetTestOptions(
    {   IODevice      => 'MS',                                    # for loginform
        RespondTo     => 'MS: Simulate sending to none: (.*)',    # auto reponder / go to next step at reception
        ResponseHash  => \%rData,                                 # to find the right response

        Time1Name     => 'Sending',
        Time1Regex    => qr{MS:\sSimulate\ssending},
        Time2Name     => 'Reception',
        Time2Regex    => qr{ParseFrameStart\s\(RTU\)\sextracted\sid},
    }                               
);                             


fhem 'get M1 SolarTemp';        # will cause step 1 to be called when send is detected and response is simulated

sub testStep1 {
    findTimesInLog();
    FhemTestUtils_resetLogs();
   
    is(FhemTestUtils_gotEvent('M1:SolarTemp'), 1, "Event SolarTemp ...");
   
    fhem 'get M1 HeatOff';
    # read simulation is triggered when sending is seen in the log.
    # next step is called when read simulation is done.
    return 'wait';
}


sub testStep2 {
    findTimesInLog();
    FhemTestUtils_resetLogs();
    my ($commDelay, $sendDelay) = calcDelays();
   
    # check no delay between read (get SolarTemp) after Step 0 and send (get HeatOff) in step 1
    ok($commDelay < 0.1, 'normal delay from read solar temp to send get HeatOff smaller than 0.1');
   
    fhem 'attr M1 dev-timing-sendDelay 0.2';            # send in step2 should be 0.2 after send in step1
    fhem 'get M1 HeatOff';
    return 'wait';
}


sub testStep3 {
    findTimesInLog();
    FhemTestUtils_resetLogs();
    my ($commDelay, $sendDelay) = calcDelays();
   
    # check send delay between read (get HeatOff) after Step 1 and send (get HeatOff) in step 2
    ok($sendDelay >= 0.2, 'defined send delay from read HeatOff to next send get HeatOff big enough');
    ok($sendDelay < 0.25, 'defined send delay from read HeatOff to next send get HeatOff not too big');
   
    fhem 'get M5 TempWasserEin';
    return 'wait';
}
usw.


In der Konfiguration ist das IO-Device mit 'none' konfiguriert:

define MS Modbus none
attr MS verbose 4
attr MS clientSwitchDelay 0
attr MS busDelay 0

define M5 ModbusAttr 5 0
attr M5 verbose 3
attr M5 dev-timing-sendDelay 0
attr M5 dev-timing-commDelay 0
attr M5 nonPrioritizedGet 1
attr M5 obj-h256-reading TempWasserEin


in den Testfunktionen wird dann per logInform erkannt, wenn das Modbus-Modul einen Request sendet und dann wird ihm die passende (oder auch explizit fehlerhafte) Antwort untergeschoben.

Falls es jemand brauchen kann, kann ich das gerne noch ausführlicher dokumentieren.

Gruss
   Stefan