Test und Fehlersuche

Zum Test eines Modul bieten sich an:

  • Unit Tests
  • Ausführung auf dem Server

Unit Tests

Für Unit-Tests verwenden wir JUnit, der Standard in der Java-Welt. Für die Datenbank setzen wir eine In-Memory H2 Datenbank ein, dadurch sieht die Testwelt für das Modul genauso aus wie später im Server.

Wir leiten unsere Test-Klasse wieder von einer Basisklasse ab, die die Testvorbereitungen wie das Aufsetzen und Befüllen der notwendigen Tabellen übernimmt und eine Hilfsmethode zum Erzeugen von Requests anbietet:

public class TestDsModDemo extends BaseTestMdule {

Die eigentlichen Tests erfolgen dann mit Methoden, die über Annotationen als Testmethoden gekennzeichnet werden. Sie sehen etwa so aus wie die folgende Methode, die eine erfolgreiche Validierung durchführt:

@Test
public void testValidateSuccess() throws Exception {
    ServerRequest request = loadServerRequest("src/test/resources/validateSuccess.json");
    TestDsModDemo module = new TestDsModDemo();
    Module wrapperModule = new WrapperModule(module);
    ServerResponse response = wrapperModule.execute(request);
    String json = response.toPrettyJson();
    System.out.println(json);
    Assert.assertNull("Error must be null", response.getError());
}

Es wird zunächst ein ServerRequest aus einer Json-Datei sowie eine Instanz unseres Moduls erzeugt. Mit Hilfe eines Wrapper-Moduls erfolgt dann der Aufruf der "execute"-Methode. Abschließend wird das Ergebnis auf die Konsole ausgegeben und auf erwartete Ergebniswerte überprüft.

Andere Test-Methode prüfen eine Validierung (testValidateFailed), die zu einem Fehler führt, oder starten eine Transformation (testRun).

Ein Herzstück dieser Tests sind die Json-Dateien. Sie wurden per Logging von GUI Aufrufen ermittelt und gespeichert (siehe weiter unten).

{
  "requestId" : 986,
  "command" : "Validate",
  "payload" : {
    "sheet" : {
      "nodes" : [ {
        "id" : 1002,
        "type" : 0,
        "attributes" : {
          "module" : "write_csv",
          "name" : "Export Person",
          "action" : "SELECT id\n     , fname\n     , lname\n  FROM public.person",
          "Append" : "Y",
          "Delimiter" : ",",
          "moduleCommand" : "DsModDemo"
        }
      }, {
        "id" : 1003,
        "type" : 1,
        "attributes" : {
          "field1" : "person",
          "field2" : "PUBLIC",
          "field3" : "",
          "objectTypeId" : 1,
          "connectionId" : 1
        }
      }, {
        "id" : 1056,
        "type" : 1,
        "attributes" : {
          "field1" : "address",
          "field2" : "PUBLIC",
          "field3" : "",
          "objectTypeId" : 1,
          "connectionId" : 1
        }
      }, {
        "id" : 1001,
        "type" : 1,
        "attributes" : {
          "field1" : "output",
          "field2" : "/tmp",
          "field3" : null,
          "objectTypeId" : 7,
          "connectionId" : 1
        }
      } ],
      "edges" : [ {
        "from" : 1003,
        "to" : 1002
      }, {
        "from" : 1056,
        "to" : 1002
      }, {
        "from" : 1002,
        "to" : 1001
      } ]
    }
  },
  "loglevel" : "FINEST",
  "variableList" : [ {
    "variableName" : "dsmod_template.ftl",
    "variableType" : "STRING",
    "variableValue" : "[#assign\nhas_equal_operator = [\"MySQL\"]?seq_contains(\"${database_product_name}\")\nhas_distinct = [\"PostgreSQL\", \"Snowflake\"]?seq_contains(\"${database_product_name}\")\nhas_decode = [\"PostgreSQL\", \"Oracle\", \"H2\", \"EXASolution\", \"Teradata\", \"Snowflake\", \"DB2\"]?seq_contains(\"${database_product_name}\")\nhas_boolean = [\"PostgreSQL\"]?seq_contains(\"${database_product_name}\")\nhas_merge_matched_and = [\"Microsoft SQL Server\", \"Snowflake\", \"H2\"]?seq_contains(\"${database_product_name}\")\nhas_merge_matched_where = [\"Oracle\", \"H2\", \"EXASolution\"]?seq_contains(\"${database_product_name}\")\nhas_delete_from = [\"Teradata\", \"Microsoft SQL Server\"]?seq_contains(\"${database_product_name}\")\nhas_delete_using = [\"PostgreSQL\", \"Snowflake\"]?seq_contains(\"${database_product_name}\")\nhas_multiple_in_cols = [\"Oracle\", \"EXASolution\"]?seq_contains(\"${database_product_name}\")\nhas_update_from = [\"PostgreSQL\", \"Snowflake\", \"Teradata\", \"Microsoft SQL Server\"]?seq_contains(\"${database_product_name}\")\n]\n[#function is_equal left_value right_value is_nullable]\n  [#if has_equal_operator][#return \"${left_value} <=> ${right_value}\"]\n  [#elseif !is_nullable][#return \"${left_value} = ${right_value}\"]\n  [#elseif has_distinct][#return \"${left_value} IS NOT DISTINCT FROM ${right_value}\"]\n  [#elseif has_decode][#return \"DECODE(${left_value}, ${right_value}, 1, 0) = 1\"]\n  [#elseif has_boolean][#return \"COALESCE(${left_value} = ${right_value}, ${left_value} IS NULL AND ${right_value} IS NULL)\"]\n  [#else][#return \"CASE WHEN ${left_value} = ${right_value} THEN 1 WHEN ${left_value} IS NULL AND ${right_value} IS NULL THEN 1 ELSE 0 END = 1\"]\n[/#if]\n[/#function]\n[#function concat_comma column]\n  [#local res = \"\"]\n  [#list column as col]\n    [#if res != \"\"]\n      [#local res += \", \"]\n    [/#if]\n    [#local res += col.name]\n  [/#list]\n  [#return res]\n[/#function]\n[#function add_map col value]\n  [#local result=[]]\n  [#if col?has_content]\n    [#local result = result + [{\"name\": col[0].name, \"value\": value}]]\n  [/#if]\n  [#return result]\n[/#function]\n[#function get_current_datetime]\n  [#if [\"Oracle\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"SYSDATE\"]\n  [#elseif [\"Microsoft SQL Server\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"Getdate()\"]\n  [#elseif [\"MySQL\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"NOW()\"]\n  [/#if]\n  [#return \"DATE_TRUNC('second', CURRENT_TIMESTAMP)\"]\n[/#function]\n[#function get_current_date]\n  [#if [\"Oracle\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"TRUNC(SYSDATE)\"]\n  [#elseif [\"Microsoft SQL Server\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"CONVERT(DATE, Getdate())\"]\n  [#elseif [\"MySQL\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"CURDATE()\"]\n  [/#if]\n  [#return \"DATE_TRUNC('minute', CURRENT_TIMESTAMP)\"]\n[/#function]\n[#function string_to_datetime string]\n  [#if [\"Oracle\", \"EXASolution\", \"H2\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"TO_DATE('${string}', 'YYYY-MM-DD HH24:MI:SS')\"]\n  [#elseif [\"PostgreSQL\", \"Snowflake\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"timestamp '${string}'\"]\n  [#elseif [\"Microsoft SQL Server\", \"Snowflake\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"CAST ('${string}' AS DATETIME)\"]\n  [#elseif [\"MySQL\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"STR_TO_DATE('${string}', '%Y-%m-%d %H:%i:%s')\"]\n  [/#if]\n  [#return \"CAST ('${string}' AS TIMESTAMP)\"]\n[/#function]\n[#function string_to_date string]\n  [#if [\"Oracle\", \"EXASolution\", \"PostgreSQL\", \"H2\", \"Snowflake\" ]?seq_contains(\"${database_product_name}\")]\n    [#return \"TO_DATE('${string}', 'YYYY-MM-DD')\"]\n  [#elseif [\"MySQL\"]?seq_contains(\"${database_product_name}\")]\n    [#return \"STR_TO_DATE('${string}', '%Y-%m-%d')\"]\n  [/#if]\n  [#return \"CAST ('${string}' AS DATE)\"]\n[/#function]\n[#function interpret x]\n   [#local s][@x?interpret/][/#local]\n   [#return s]\n[/#function]"
  }, {
    "variableName" : "batch_instance_id",
    "variableType" : "LONG",
    "variableValue" : "-1"
  }, {
    "variableName" : "schedule_date",
    "variableType" : "TIMESTAMP",
    "variableValue" : "2020-07-19T09:13:28.309Z"
  }, {
    "variableName" : "action_id",
    "variableType" : "LONG",
    "variableValue" : "1002"
  }, {
    "variableName" : "request_id",
    "variableType" : "LONG",
    "variableValue" : "986"
  } ]
}

Die Testklassen und Testresourcen sind ebenfalls im Zip-File enthalten.

Ausführung auf dem Server

Bei der Ausführung auf dem Server gibt es verschiedene Möglichkeiten der Fehleranalyse.

  • Ausführung durch den Server
  • Direkter Aufruf
  • Konsolausgaben und Logging
  • Remote Debugging

Ausführung durch den Server

Am einfachsten ist natürlich, das Modul im Server einzubinden und es über die GUI zur Validierung und Ausführung anzusprechen. Wenn dann alles einwandfrei funktioniert, darf man sich zufrieden auf die Schulter klopfen.

Direkter Aufruf

Manchmal klappt es aber nicht auf Anhieb, in diesem Fall kann man zunächst das Modul auf dem Server mit Hilfe seines Run-Skripts und eines geeigneten Json-Request stimulieren.

Dazu verwenden wir wieder den Request, den wir auch schon für die Unit-Tests entwickelt hatten. Nachdem er auf den Server kopiert wurde, kann man ihn als Input mitgeben:

runJavaModule.sh DsModDemo < validateSuccess.json 2> output

Dieses Verfahren kann insbesondere dann hilfreich sein, wenn Probleme beim Entwickeln der Aufruf-Skripts auftreten. Man kann dann einfach Debug-Ausgaben in das Skript aufnehmen oder sogar jeden Befehl vor der Ausführung anzeigen, um Probleme zu analysieren.

Für das Anzeigen der Befehle vor ihrer Ausführung kann man das Skript runJavaModule.sh anpassen und einfach folgende Zeilen am Beginn des Skripts ergänzen:

#/bin/bash

exec 4>&1
BASH_XTRACEFD=4
set -x

Jetzt werden die Bash Debug-Ausgaben durch "set -x" an den StdOut-Kanal geschickt und man kann an der Konsole alles verfolgen:

$ runJavaModule.sh DsModDemo < validateSuccess.json 2> output
++ set -o nounset
++ set -o errexit
++ DEBUG=0
++ PORT=9000
++ getopts :c:dhj:p:v opt
++ shift 0
++ '[' 1 -eq 0 ']'
++ MODULE_CLASS=DsModDemo
++ MODULE_NAME=DsModDemo
++ export 'CLASSPATH=/home/datasqill/lib/DsModDemo.jar:/home/datasqill/lib/datasqill-api.jar:/home/datasqill/lib/jdbc/*'
++ CLASSPATH='/home/datasqill/lib/DsModDemo.jar:/home/datasqill/lib/datasqill-api.jar:/home/datasqill/lib/jdbc/*'
++ export JAVA_OPTS=-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver
++ JAVA_OPTS=-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver
++ export 'JAVA_OPTS=-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver -Dde.softquadrat.datasqill.mappertool=getkey'
++ JAVA_OPTS='-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver -Dde.softquadrat.datasqill.mappertool=getkey'
++ export 'JAVA_OPTS=-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver -Dde.softquadrat.datasqill.mappertool=getkey -Djava.security.egd=file:/dev/urandom'
++ JAVA_OPTS='-Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver -Dde.softquadrat.datasqill.mappertool=getkey -Djava.security.egd=file:/dev/urandom'
++ [[ 0 -eq 0 ]]
++ DEBUG_OPTS=
++ [[ -f /home/datasqill/lib/DsModDemo.env ]]
++ exec java -cp '/home/datasqill/lib/DsModDemo.jar:/home/datasqill/lib/datasqill-api.jar:/home/datasqill/lib/jdbc/*' -Djdbc.drivers=net.snowflake.client.jdbc.SnowflakeDriver:org.apache.hive.jdbc.HiveDriver -Dde.softquadrat.datasqill.mappertool=/home/datasqill/bin/getkey -Djava.security.egd=file:/dev/urandom -Dtomee.port=17491 -Dtomee.shutdown_port=8005 -Dtomee.shutdown_command=SHUTDOWN DsModDemo

Konsolausgaben und Logging

Um herauszufinden, warum das Modul nicht das tut, was es soll, kann man Konsolausgaben und Logging im Code ergänzen. Das sind Ausgaben der Form

System.out.println("Meine Nachricht auf die Standard Ausgabe ...");
System.err.println("Meine Nachricht auf die Fehler Ausgabe ...");

oder

Logger logger = Logger.getLogger("de.softquadrat.datasqill.modules");
logger.info("Meine Nachricht in das Modul-Log");

Module verwenden die Standard-Logklassen von Java (java.util.logging). Während die Standardkanäle (stdout, stderr) vom Server an die GUI zurückgeschickt werden, werden Log-Nachrichten allerdings normalerweise verworfen. Damit auch die Log-Ausgaben gespeichert und am Ende in der GUI angezeigt werden, muss man einen Konfigurationseintrag im datasqill Repository anlegen, der das Speichern dieser Ausgaben für eine bestimmte Transformation (Action) einschaltet.

Das machen wir wieder über das Deployment-Skript. Wir bestimmen die Id unserer Action mit Hilfe der GUI, erzeugen ein Json-File und laden es in den Server:

{
  objectList:
  [
    {
      dsdbFormat: 1
      deploymentType: version
      current:
      {
        version: 3.2.2
        data:
        [
          {
            target: VV_SQTS_CONFIG_GLOBAL
            columns: [ "config_key1", "config_key2", "config_key3", "config_key4", "config_value", "config_sort", "config_comment" ]
            rows: [
              [ "SERVER", "MODULE", "LOGLEVEL", "1215", "FINEST", 1000, "Enable logging with level finest for action" ]
            ]
          }
        ]
      }
    }
  ]
}
deploySchema.sh < debug1002.dsdb

Danach erscheinen beim Validieren und beim Ausführen die Log-Ausgaben in der GUI (Reiter StdOut Log unten). Hier ein Beispiel-Validierungsdialog:

ValidationLogs.png

Remote Debugging

Remote Debugging erlaubt die Fehleranalyse mit Hilfe einer IDE wie Eclipse oder IntelliJ. Dabei wird der Prozess auf dem Server im Debug-Mode gestartet und die IDE mit ihm verbunden. Anschließend kann man die Vorzüge einer Entwicklungsumgebung nutzen, um Breakpoints zu setzen, Variablen zu inspizieren und sogar zu modifizieren oder sich schrittweise durch das Modul zu bewegen.

Es werden drei Zutaten für das Remote Debugging benötigt:

  • Verwendung eines Debug-Ports
  • Start der Modul VM im Debug-Mode
  • Starten der Debug-Sitzung in der IDE

Der Debug-Port ist ein beliebiger, freier Port auf dem Server. Er muss vom Client mit der Entwicklungsumgebung erreichbar sein, notfalls über einen SSH-Tunnel. Das Skript "runJavaModule.sh" verwendet den Port 9000.

Damit die JVM des Moduls im Debug-Mode startet, muss man die Option "-d" beim Aufruf des Skripts "runJavaModule.sh" mitgeben:

runJavaModule.sh -d DsModDemo < validateSuccess.json 2> output

Danach können wir eine Remote-Debugsitzung starten. Bei Eclipse wählt man zunächst den Menüpunkt Run - Debug Configurations... und wechselt dort zu Remote Java Application

Debug1.png

Dort wird mit New Configuration ein neuer Eintrag angelegt und die Koordinaten des entfernten datasqill Servers angegeben:

Debug2.png

Jetzt setzt man einen Breakpoint im Modul und startet den Debugger. Er verbindet sich mit dem Serverprozess und stoppt an der gewünschten Stelle:

Debug3.png