Giter Club home page Giter Club logo

polarize's Introduction

Why polarize?

polarize has several goals:

  • Make the source code where testcase definitions and requirements live
  • A TestNG reporter that generates junit reports compatible with the XUnit importer to update TestRun results
  • Creates or updates Polarion TestCase based on Java annotations during compilation
  • A Java tool to send the XUnit and Testcase importer requests (no shelling out to curl)
  • A Java tool to consume importer messages from the CI Message Bus (no subscribe.py)

Writing Requirements and TestCases in Polarion by hand is very time consuming. Being able to auto-generate a Polarion TestCase or Requirement given metadata that already exists in an external format is more ideal. Furthermore, it is better to have a single source of truth (the source code) and have this reflected in Polarion, rather than try to maintain and keep synchronized two sets of data and somehow link the Polarion ID to the test method.

Due to performance limitations we are required to submit TestRun results via a batch operation. This batch operation uses a modified xunit style XML file which gets POST'ed to a REST service endpoint on the Polarion plugin. Generating this xunit importer compatible file is tricky for teams using TestNG as their framework since the junit reports it generates lacks some crucial information, like pass/fail at the testcase level, or parameterized arguments. And of course, it also does not supply the modifications needed by the XUnit importer.

How does it work?

Currently, the heart of polarize lies in these concepts:

  1. A custom annotation processor
  2. XML description files (generated via the custom processor) that describe the TestCase
  3. A mapping.json file that maps class.methodName to a unique TestCase ID
  4. A custom IReporter class for TestNG that generates special XUnit result files

In essence, polarize uses a custom annotation processor to supply metadata of your classes and test methods. Given the correct annotation data, when your source is compiled and the polarize jar is in your classpath, then the annotation processor will run. As it runs, it will find the elements which were annotated and generate XML equivalents of that annotation data. In fact, these XML files are the files that are used by the CI Ops Testcase Importer tool. These XML files serve 2 purposes:

  • Create the Polarion TestCase if the ID is empty
  • A (tertiary) way to map the test method to the Polarion Testcase if the ID is not in the annotation or mapping.json

The annotations exist to generate the XML equivalent description, as well as documenting metadata of the method itself. The annotations generate XML descriptions, which are (arguably) more human readable, and can be used by upstream contributors to make comments on.

One of the requirements for the XUnit importer is that each <testcase> must have a polarion ID or custom ID set. This is to map the testcase to the Polarion ID. The XML files that polarize generates will either contain the ID or not. As the annotation processor runs, it will use an algorithm to map the qualified name of the testmethod to where the XML file should be. The algorithm is:

  1. Check the annotation testCaseID and see if it is an empty string
  2. Look for the XML description file in path Project/ClassName/MethodName(.xml) and get the testcase id attribute
  3. Check the mapping.json file with the key being the class.methodName of the method being processed

Once all three pieces of information are gathered, it will do the following:

annotation xml mapping Action(s) Name
0 0 0 Make import request NONE
0 0 1 Edit the XML file, add to badFunction MAP
0 1 0 Edit the Mapping file, add to badFunction XML
0 1 1 Verify equality, add to badFunction XML_MAP
1 0 0 Edit the XML and mapping file ANN
1 0 1 Verify equality, edit the XML ANN_MAP
1 1 0 Verify equality, add to mapping ANN_XML
1 1 1 Verify equality ALL

The annotation column checks what was described in bullet 1, the xml column does what was in bullet 2, and the mapping column does what was in bullet number 3 above. If the ID exists for the annotation, xml or mapping, a 1 (or true) is marked for that column, otherwise it is 0. The Action(s) column describes what happens in each row. There are eight rows since there are 3^2 possibilities. In the Action(s) column, if it says "add to badFunction", this is a temporary file that will be created in /tmp/badFunction.txt, and it will give the class.methodName of every test case it finds where the ID exists either in the xml or mapping.json, but the ID was not given in the annotation. This gives the test author the opportunity to go back and add a newly generated Polarion ID back into the source code.

XML Description File

Here's an example of an annotation generated XML file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<testcase id="PLATTP-9520" level="component" posneg="negative" importance="high" automation="automated">
    <title>com.github.redhatqe.rhsm.testpolarize.TestReq.testUpgradeNegative</title>
    <description>Test for reporter code</description>
    <nonfunctional subtype1="compliance" subtype2="-"/>
    <setup>Description of any preconditions that must be established for test case to run</setup>
    <teardown>The methods to clean up after a test method</teardown>
</testcase>

From the following annotation:

    @TestDefinition(projectID=Project.PLATTP,      // required
                  testCaseID="PLATTP-9520",            // if empty or null, make request to WorkItem Importer tool
                  importance=DefTypes.Importance.HIGH, // defaults to high  if not given
                  posneg=PosNeg.NEGATIVE,              // defaults to positive if not given
                  level= DefTypes.Level.COMPONENT,     // defaults to component if not given
                  // linkedWorkItems is optional and allows you to specify another (existing) WorkItem.
                  linkedWorkItems={@LinkedItem(workitemId="PLATTP-10348",         // If linkedWorkItems used, required
                                   project=Project.PLATTP,                        // What Project to go under
                                   role=DefTypes.Role.VERIFIES)},                 // Role type
                  // If testtype is FUNCTIONAL, subtype1 and 2 must be of type EMPTY.
                  testtype=@TestType(testtype= DefTypes.TestTypes.NONFUNCTIONAL,  // Defaults to FUNCTIONAL
                                     subtype1= DefTypes.Subtypes.COMPLIANCE,      // Defaults to EMPTY (see note)
                                     subtype2= DefTypes.Subtypes.EMPTY),          // Defaults to EMPTY (see note)
                  setup="Description of any preconditions that must be established for test case to run",
                  tags="tier1 some_description",
                  teardown="The methods to clean up after a test method",
                  update=true,
                  automation=DefTypes.Automation.AUTOMATED)  // if not given this defaults to AUTOMATED)
    @Test(groups={"simple"},
          description="Test for reporter code",
          dataProvider="simpleProvider")
    public void testUpgradeNegative(String name, int age) {
        AssertJUnit.assertEquals(age, 44);
        Assert.assertTrue(name.equals("Sean"));
    }

The mapping.json file

As well as generating the test definition XML file, at the beginning of the source code processing, polarize will load a mapping file in a JSON format (or if this is the first time compiling with polarize, or if the mapping file is missing it will generate the mapping file). Rather than do a cumbersome look up algorithm of qualified test name to its matching test definition XML file, and then loading this file to obtain the Polarion ID, the mapping file is loaded once into memory, and used for any look up of the qualified test method name to its matching Project ID and Polarion ID.

Note that this is a 2-level mapping of qualifiedName -> ProjectID -> PolarionID, since a method can and probably does exist in more than one Project, it will also have one or more corresponding Polarion TestCases. The mapping file also contains an array of the names of the parameters used for this method if it is a parameterized test. This is so that the xunit report generator can also read in this file to properly generate the parameterization information for each test method that was executed in the TestRun.

Xunit report generation

Speaking of this, polarize also comes with a class XUnitReporter that implements TestNG's IReporter interface. By setting your TestNG test to use this Reporter, it will generate the xunit which is compatible with the XUnit Importer. In fact, if your tests use Data Providers, it will also generate the parameterized data for you too with the previously mentioned mapping.json file.

After you execute your TestNG test, you will see a report generated like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<testsuites>
    <properties>
        <property name="polarion-user-id" value="stoner"/>
        <property name="polarion-project-id" value="PLATTP"/>
        <property name="polarion-set-testrun-finished" value="true"/>
        <property name="polarion-dry-run" value="false"/>
        <property name="polarion-include-skipped" value="false"/>
        <property name="polarion-response-rhsm_qe" value="stoner"/>
        <property name="polarion-custom-notes" value="file:///some/otherpath"/>
        <property name="polarion-custom-jenkinsjobs" value="http://some/path"/>
        <property name="polarion-testrun-title" value="Sean Toner Polarize TestRun"/>
        <property name="polarion-testrun-id" value="Polarize TestRun"/>
    </properties>
    <testsuite name="Sanity Test" tests="2" errors="2" time="0.0" skipped="0">
        <testcase name="testUpgradeNegative" classname="com.github.redhatqe.rhsm.testpolarize.TestReq" status="success">
            <properties>
                <property name="polarion-testcase-id" value="PLATTP-9520"/>
                <property name="polarion-parameter-name" value="Sean"/>
                <property name="polarion-parameter-age" value="44"/>
            </properties>
        </testcase>
        <testcase name="testUpgradeNegative" classname="com.github.redhatqe.rhsm.testpolarize.TestReq">
            <failure/>
            <properties>
                <property name="polarion-testcase-id" value="PLATTP-9520"/>
                <property name="polarion-parameter-name" value="Toner"/>
                <property name="polarion-parameter-age" value="0"/>
            </properties>
        </testcase>
    </testsuite>
</testsuites>

Why not just write requirements and testcases directly in polarion?

  • Need an extra license for Product Manager to enter in information and developer to review
  • Bypasses the test project as the canonical source of truth
  • Couples requirement/testcase definition to a specific implementation (Polarion)
  • Polarion does not allow upstream communities to review or submit requirements/workitems
  • Harder to review because it's not a plain text file (no diffs)
  • At the mercy of eng-ops to update/restore information about a work item

Polarize solves all these problems

  • Because requirements/testcases are external files, dont need extra licenses to review or enter
  • Because workitems are represented by external files, they can be kept in source control (single canon of truth)
  • Decouples definition of requirements and testcases from a specific implementation
  • Because requirements and testcases belong in source control, they can be reviewed by upstream community
  • Since annotations generate plain text xml, it is easy to diff and review
  • Because the xml descriptions are external, they can always be used to update existing tests on-demand

Furthermore, TestCase and Requirements can be generated automatically whenever code is compiled. This does not mean that the imported WorkItem is automatically approved. It just means they will be imported into Polarion in a draft state.

How do you use polarize?

The basic premise of polarize is that given proper annotation of the source code and a place to look for xml description files or features files, all the mapping from a requirement/testcase to the test method can be done.

Configuring polarize

Over time, polarize has gained a number of required settings that need to be configured. It moved from a simple properties style file to the now current XML based configuration. Work was planned on a YAML based configuration, but although deserializing a YAML file to a POJO wasn't too hard, serializing a POJO into YAML turned out to be difficult.

There are 2 main configuration files

  1. src/main/resources/polarize-config.xml
  2. ~/.polarize/polarize-config.xml

The latter (if it exists) will override the keys in the former. It is recommended to use the latter because you will need to put passwords for 2 different servers in it, and you won't want to accidentally check these in.

The config file has some documentation for what the various settings are used for and is hopefully self-documenting.

How to use polarize

This section will describe how to make use of polarize

Example polarize-config.xml file

<config>
  <!-- The name of your project or team.  Can be used to replace {project-name} in the 
  <testrun id="{project-name}"> attribute as a default -->
  <project-name name="RHSM-QE"/>
  <!-- The value of basedir can be used like <requirements-xml path="{basedir}/requirements"-->
  <basedir path="/home/stoner/Projects/rhsm-qe"/>
  <!-- Where Requirements XML are.  Eg <requirements-xml path="/home/stoner/Projects/rhsm-qe/requirements"/> -->
  <requirements-xml path="{basedir}/requirements"/>
  <!-- Where TestCase XML are.  Eg. <testcases-xml path="/home/stoner/Projects/rhsm-qe/testcases"/> -->
  <testcases-xml path="{basedir}/testcases"/>
  <!-- Path of the mapping.json that maps the qualified name to project -> id -->
  <mapping path="{basedir}/mapping.json"/>
  <author>ci-user</author>
  <project>RHEL6</project>
  <servers>
    <server name="polarion"
            url="https://path.to.your.polarion/polarion"
            user="foo"
            password="____________"/>
    <server name="polarion-devel"
            url="https://path.to.your.polarion-devel/polarion"
            user="foo"
            password="____________"/>
    <server name="kerberos"
            user="foo"
            password="____________"/>
    <server name="ossrh"
            user="ossrh-user"
            password="ossrh-pw"/>
    <server name="broker"
            url="tcp://your.broker:61616"/>
  </servers>
  <importers>
    <importer type="testcase"><!-- settings for the testcase importer -->
      <endpoint route="/import/testcase"/>
      <!-- The path for where the generated xml filed used for TestCase importer will be created --> 
      <file path="/tmp/testcases.xml"/>
      <!-- Creates the JMS selector
       The -->
      <selector name="rhsm_qe" val="testcase_importer"/>
      <!-- An optional prefix and suffix.  If none is given, the qualified name of the method is the title -->
      <title prefix="RHSM-TC : " suffix=""/>
      <!-- time in milliseconds to wait for message reply -->
      <timeout millis="300000"/>
      <!-- If true, when polarize needs to create a new TestCase in Polarion it will make an import request.  If false, 
        even if polarize detects that a new TestCase should be made in Polarion, it will skip making a request. The XMl 
        will still be generated, but since an import request was not made, both the annotation and XML id will be empty.
        This means once enabled is set to true, polarize will actually do the request to make a new Polarion TestCase-->
      <enabled>false</enabled>
    </importer>
    <importer type="xunit"><!-- # settings for the xunit importer -->
      <!-- id is an optional unique id for testrun. Defaults to a timestamp (uniqueness guaranteed by client)
         title is the (possibly non-unique) name of the testrun.-->
      <template-id id=""/>
      <!-- By default, this will simply be the name of the project plus a time stamp.  To make it easier to find this 
        test run in Polarion, the id should be given something more, such as a compose ID, or the version of the 
        artifact being tested.  For example: "RHEL 7.4 Server 20170314 14:57:31-->
      <testrun id="{project-name}" title=""/>
      <endpoint route="/import/xunit"/>
      <file path="{basedir}/test-output/testng-polarion.xml"/>
      <!-- # the JMS selector <name>='<value>' -->
      <selector name="rhsm_qe" val="xunit_importer"/>
      <!-- A list of key-value pairs.  The response properties are used by the xunit importer -->
      <test-suite>
          <property name="dry-run" val="false"/>
          <property name="set-testrun-finished" val="true"/>
          <property name="include-skipped" val="false"/>
      </test-suite>
      <!-- These are custom fields in the Polarion TestRun -->
      <custom-fields>
        <property name="plannedin" val=""/><!-- The plannedin phase -->
        <property name="jenkinsjobs" val=""/><!-- Path to the jenkins job -->
        <property name="notes" val=""/><!-- arbitrary field -->
      </custom-fields>
      <!-- time in milliseconds to wait for reply message -->
      <timeout millis="300000"/>
      <!-- If true, once xunit report is generated, perform an XUnit Import request to send the results to Polarion.  If
       false, do not make the XUnit Import request.  The xunit result file will still be generated however so that it 
       can be uploaded later -->
      <enabled>true</enabled>
    </importer>
  </importers>
</config>

Editing the config file or a xunit result file

There is a configurator class which can be used to edit settings in the polarize-config.xml file, or in a given xunit result file. The former is handy when you need to edit the polarize-config.xml file for long term changes, and the latter is nice to have when you only need to edit an existing xunit result file say for example to upload to Polarion.

This shows an example of modifying an existing xunit result file with other information and storing it in a new file /tmp/modified-testng-polarion.xml

java -cp ./polarize-0.5.5-SNAPSHOT-all.jar com.github.redhatqe.polarize.configuration.Configurator \
--current-xunit /home/stoner/Projects/rhsm-qe/test-output/testng-polarion.xml \
--new-xunit /tmp/modified-polarion.xml \
--testrun-id "Personal Testrun 1" \
--include-skipped true \
--property notes="A personal test run"

Here's an example of where you might want to change the polarize-config settings for a longer term purpose

java -cp ./polarize-0.5.5-SNAPSHOT-all.jar com.github.redhatqe.polarize.configuration.Configurator \
--edit-config \
--project RedHatEnterpriseLinux7 \
--template-id "sean toner master template test"

By using the --edit-config option, this will overwrite the existing polarize-config.xml file and backup the original to the ~/.polarize/backup directory as a timestamped file.

Annotations for Testcase definitions

The polarize project uses 2 new custom annotation types. The @TestDefinition annotation is used for all the metadata necessary to describe a TestCase in Polarion and is annotated on methods.

Here is a full example:

package com.github.redhatqe.rhsm.testpolarize;

import com.github.redhatqe.polarize.metadata.DefTypes;
import com.github.redhatqe.polarize.metadata.Requirement;
import com.github.redhatqe.polarize.metadata.TestDefinition;
import com.github.redhatqe.polarize.metadata.TestType;
import com.github.redhatqe.polarize.metadata.DefTypes.Project;

import org.testng.Assert;
import org.testng.AssertJUnit;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * A dummy example showing how to use the annotations.  Since these are repeating annotations, that means that the
 * java source code must be compiled at Java source of 8
 *
 * Created by stoner on 7/12/16.
 */
public class TestReq {

    @TestDefinition(projectID=Project.RHEL6,
              description="TestCase for a dummy test method",
              title="A not necessarily unique title",  // defaults to class.methodName
              reqs={})
    @TestDefinition(projectID=Project.PLATTP,          // project required
                  testCaseID="PLATTP-9520",            // if empty or null, make request to WorkItem Importer tool
                  importance=DefTypes.Importance.HIGH, // defaults to high  if not given
                  posneg=PosNeg.NEGATIVE,              // defaults to positive if not given
                  level= DefTypes.Level.COMPONENT,     // defaults to component if not given
                  // linkedWorkItems is optional and allows you to specify another (existing) WorkItem.
                  linkedWorkItems={@LinkedItem(workitemId="PLATTP-10348",         // If linkedWorkItems used, required
                          project=Project.PLATTP,                                 // What Project to go under
                          role=DefTypes.Role.VERIFIES)},                          // Role type
                  // If testtype is FUNCTIONAL, subtype1 and 2 must be of type EMPTY.
                  testtype=@TestType(testtype= DefTypes.TestTypes.NONFUNCTIONAL,  // Defaults to FUNCTIONAL
                                     subtype1= DefTypes.Subtypes.COMPLIANCE,      // Defaults to EMPTY (see note)
                                     subtype2= DefTypes.Subtypes.EMPTY),          // Defaults to EMPTY (see note)
                  setup="Description of any preconditions that must be established for test case to run",
                  tags="tier1 some_description",
                  teardown="The methods to clean up after a test method",
                  // defaults to false.  If false do not make a new import even if annotation changes.  If true, the 
                  // annotation processor will _always_ make a new import request even if nothing changes
                  update=false,  
                  automation=DefTypes.Automation.AUTOMATED)  // if not given this defaults to AUTOMATED)
    @Test(groups={"simple"},
          description="Test for reporter code",
          dataProvider="simpleProvider")
    public void testUpgradeNegative(String name, int age) {
        AssertJUnit.assertEquals(age, 44);
        Assert.assertTrue(name.equals("Sean"));
    }

    @DataProvider(name="simpleProvider")
    public Object[][] dataDriver() {
        Object[][] table = new Object[2][2];
        List<Object> row = new ArrayList<>();

        row.add("Sean");
        row.add(44);
        table[0] = row.toArray();

        row = new ArrayList<>();
        row.add("Toner");
        row.add(0);
        table[1] = row.toArray();
        return table;
    }
}

Notice that the above uses repeating annotations, a feature only available in Java 8. If your test method differs in some aspect of the TestCase definition within Polarion, then you can enter 2 or more annotations. For example, if the description, setup, teardown, or any other information differs between RHEL6 and RedHatEnterpriseLinux7 for your test method, then you should annotate it twice as above. If your definitions are otherwise identical, you can just put the projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7} for example.

Given the above annotations, and polarize-config.xml as shown earlier, polarize will do the following:

  • Look at the xmlDesc field to get path to xml description file
    • If no such file exists, look in a configured path for the xml description file
      • If this file also does not exist, generate an xml description file from the given annotation data
        • Pass this generated XML file to the WorkItem importer for it to create a Requirement/TestCase
        • Take the return response and look for the Polarion ID. Edit this value into the xml description file
        • From the information in the XML and annotation, edit the mapping.json file
  • Once the xml file is ready, we know the mapping between test method and Polarion Requirement and TestCase ID
  • When the test run is done and the junit report is created, post process the result file

Here are some more examples.

Simplified (using defaults)

    /**
     * Shows an example of TestSteps with default named Params.
     *
     * This is a test that uses a DataProvider.  The annotation processor will determine the names of the arguments
     * and use this as the names for the TestStep Parameters.  In the example below, the processor will determine that
     * this method has 2 arguments called "name" and "age".  It will create the XML necessary to include those params.
     * 
     * Notice that this also has projectID set to an array.  If the TestDefinition is identical except for the project
     * then you can set the projectID like this.  If there is any difference, then you must use a repeating annotation.
     * Note also that if the projectID is an array, that the testCaseID must 
     *
     * @param name
     * @param age
     */
    @TestDefinition(author="stoner", projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7})
    @Test(groups={"simple"},
          description="Shows how to add parameters named '0', '1', '2', etc through @TestSteps",
          dataProvider="simpleProvider")
    public void testUpgrade(String name, int age) {
        Assert.assertTrue(name.equals("Sean") || name.equals("Toner"));
        Assert.assertTrue(age < 100);
    }

    @DataProvider(name="simpleProvider")
    public Object[][] dataDriver() {
        Object[][] table = new Object[2][2];
        List<Object> row = new ArrayList<>();

        row.add("Sean");
        row.add(44);
        table[0] = row.toArray();

        row = new ArrayList<>();
        row.add("Toner");
        row.add(0);
        table[1] = row.toArray();
        return table;
    }

Making use of XUnitReporter for TestNG

Here's a little snippet of how to setup your test for debugging

gradle clean
gradle pP
gradle publishToMavenLocal
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006 \
-cp /path/to/your/polarize-1.0.0-all.jar:/path/to/your/awesome-test-1.0.0.jar \
org.testng.TestNG -reporter com.github.redhatqe.polarize.junitreporter.XUnitReporter /path/to/your/test-suite.xml

Basically, you just need to put polarize on your classpath, and add the -reporter (or in your XML suite file add the XUnitReporter under ). TestNg will take care of everything else and will generate your xunit file usable but the XUnit importer.

Listening to CI Bus messages

Because the importers (XUnit and TestCase) send replies back as a message on the CI Bus, it is necessary to listen for these messages. The polarize-config.xml file contains entries like this:

    <importer type="testcase">
      <!-- ellided entries  -->
      <selector name="rhsm_qe" val="testcase_importer"/><!-- Creates the JMS selector -->
      <!-- ellided entries -->
    </importer>

The <selector> element contains 2 attributes named name and val. The JMS selector used to filter out all the messages on the bus uses the format "{name}='{val}'". There are selector elements for both the testcase and xunit importers.

For command line usage of the XUnit Importer, one can override the config file settings by using the --selector option. It should take an argument like --selector "name='val'". For example:

java -cp ./polarize-0.5.4-all.jar com.github.redhatqe.polarize.junitreporter.XUnitReporter --selector "rhsm_qe='my_tag'"

Embedding in a java project

TODO Show how to use the CIMessageBusListener in a java project and as a standalone tool

Message Status

In the case of the XUnitReporter, the JSON message will look like this on success:

{
  "testrun-url" : "https://mypolarion.server.com/polarion/#/project/RHEL6/testrun?id=RHEL6%202016-11-04%2001-31-39-498",
  "import-results" : [ {
    "suite-name" : "CLI: BashCompletion Tests",
    "status" : "passed"
  } ],
  "status" : "passed",
  "log-url" : "http://my.logstash.server/polarion/RHEL6/20161104-013138.523.log"
}

POST to the importers

polarize can be used as a standalone tool to make an importer request, or it can be embedded in another java project.
There is no curl call for the POST'ing of the XML to the endpoint. Here's an example of manually sending an XUnit request using the XUnitReporter class

TODO Give examples of how to run the XUnit and TestCase importers as a standalone tool, and how to embed in another java project

Using polarize with clojure or other dynamic JVM languages

Since polarize's main use was for Java annotations that could be processed at compile time, this doesn't make sense for clojure. When you run lein compile, even though polarize is in the classpath, it does not run the annotation processor. In theory, this should work, since annotation processors can examine either java source code or bytecode (and lein compile generates .class files), but I was unable to get leiningen to "hook" into our clojure code. I even tried modifying the testng-clj project's gen-class-testng macro, since it is the macro that runs gen-class on all the clojure code to generate the java classes, and I told it to also accept any method annotated with a TestDefinition annotation. But even that did not cause the TestDefinitionProcessor class to be executed on the bytecode.

So an alternative solution was to grab the annotation information at runtime. All annotations have a retention policy. An annotation can only exist at compile time, or it can exist at runtime. The latter is useful if your code uses reflection so that you can use annotations as metadata. To use the annotation at runtime, you need to compile your project code as an uberjar (aka fatjar). You then need to put this uberjar on the classpath with polarize, and also add it as a command line option. Since polarize uses gradle as its build tool, it's a little tricky to get it to return the classpath (ie, the equivalent of lein classpath). There is a task I created called classPath which will display the classpath, but also some extraneous information. I just copy what I need and export it into a var:

Once you have saved off the classpath to a var (say $CP), you can run the annotation processing like this:

cd /path/to/rhsm-qe
export CP=`lein classpath`
export CP="$CP:${HOME}/.m2/repository/com/github/redhatqe/polarize/polarize/0.5.5-SNAPSHOT/polarize-0.5.5-SNAPSHOT-all.jar
lein clean
lein uberjar
java -cp $CP -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5009 com.github.redhatqe.polarize.JarHelper \
--jar file:///home/stoner/Projects/rhsm-qe/target/sm-1.1.0-SNAPSHOT-standalone.jar \
--packages "rhsm.gui.tests,rhsm.cli.tests"

The --jar option specifies the path of the uberjar, and the --packages is a comma separated list of packages to scan for. Note that this example also sets the debugger option so that you can setup a remote debug configuration if needed.

Debugging

Speaking of debugging, if you need to debug your clojure code you can set in your project.clj file the following:

:jvm-opts ["-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5007"]

In your Intellij editor, you can set up a remote debug profile and have it listen to port 5007. Once the IDE connects, the rhsm-qe tests will run, and will stop once it hits any break points you set in your IDE

Roadmap

Discussion on what still needs to be done.

Feature file parser

BDD style requirement files are becoming very common, and gherkin style file the defacto standard. If you are unfamiliar with feature files and the gherkin syntax, please see https://cucumber.io/docs/reference

Although feature files are good at capturing a high level feature, it's setup requirements, scenario and assertions for testing, there is still data required for Polarion that needs to be contained within the feature file (for example if it's a positive or negative test, if it is a functional or non-functional test, or whether it's a must have, should have etc feature).

This extra data can be contained as a parseable entity within the Feature description. Here's an example:

Feature: Generate XML from annotated class

  Definition:
    severity: must_have      # One of should_have, must_have, nice_to_have, will_not_have
    type: Functional         # One of Functional, NonFunctional,
    subType:                 # optional
    status: draft            # defaults to draft
    title:                   # Not filled in by PM.  Done by QE
    plannedin                # a list of plannedin releases
      - RHEL_7_3
    links
      - /some/link/to/share  # optional

  Scenario: Annotated class generates valid XML
    Given The class is annotated with @TestDefinition
      And The TestDefinition annotation has author
      And The TestDefinition annotation has projectId
      And The TestDefinition annotation has sub-annotation Requirement
      And The field for xmlDesc is an empty string in the Annotation
     Then XML suitable for the WorkItem importer will be generated

Workflow process: Feature file to Requirements

Polarize is somewhat opinionated in how it should be used. The central concept is that when a QE needs to write an automated test, there should already be an existing definition of the requirements. More and more companies are moving to BDD style tests and feature files to capture the essence of a feature and what needs to be tested. Generally speaking, the Product Owner, developers, and Quality Engineers should go over what needs to be done, and through this discussion they will write a gherkin style feature file.

Once the initial feature file is hashed out between the PO, devs, and QE the written feature file can be stored in source control within the test project. Any subsequent changes needed can be reviewed in source control whether the change comes due to developers gaining a better understanding of the requirement, QE's finding that there is not enough information to be able to make assertions, or the PO getting additional feedback from customers. This reviewal process can be done under normal source control abilities (eg, through github PR's or gerrit for example). Once the review has been done the feature file will be merged in including some additional metadata.

This metadata will only be to cover information needed for a Polarion Requirement or TestCase. By default, polarize will look for a feature file based on the following path:

requirements.xml.path/<project>/<class>/<methodName>.feature

Limitations and known TODOs

Here is a compilation of known limitations and TODO's. Keep these in mind when using polarize

Editing the polarize-config.xsd

If you ever need to edit the polarize-config.xsd file to add new configuration properties, you will need to cd into the src/main/resources/configuration directory, and then run the configuration.sh script. Due to some weirdness with JAXB the generated class for the ConfigType does not get annotated with @XmlRootElement(name="config"). So remember to manually add this to the ConfigType class.

There appears to be some workarounds by using the ObjectFactory to marshall/unmarshall classes that are not annotated with the XmlRootElement, but I have not looked into that method.

Unique method names

One limitation of polarize is that it assumes that there are no overloaded testmethods. This is because polarize maps the qualified name of a test method to a file on the system. If you have an overloaded method (a method with the same name but different type signature) then there will no longer be a one-to-one mapping from qualified method name to a file. Note that this does not apply to data driven tests. It is perfectly fine to have a test method with data driven parameters.

Many methods to one testcase

The XUnit Importer has the ability to do different lookups. There are 3 possibilities:

  • id (default): Uses the Polarion TestCase ID
  • custom: Allows the user to have a custom (but unique) ID of method -> customID
  • name: If the class.name is unique, then this can be used

The last lookup by name was added after polarize had already (mostly) been written. However, the name lookup also will not work where you have several test methods which verify the same TestCase. For example if you have more than one way of doing essentially the same test. Some might argue that there should be a one-to-one mapping, but TestCases should be somewhat implementation agnostic.

Setting your own title and xml path

The following annotation fields do not yet currently work:

  • title
  • xmlDesc

These fields were included as a possible workaround (that still needs to be fully implemented ) for the limitation of a unique method name. By specifying a custom title and xmlDesc in the annotations, polarize would use those as the means to map the test method to the Polarion ID. If you do have overloaded methods, then you must supply a (unique) file system path. If no file exists, polarize will generate it there. When it needs to get the Polarion ID, it will read in this file (which is why the path must be unique for each method). This is the reason that the title and xmlDesc do not yet work, because some additional work still needs to be put into place.

Fragility of mismatched testcase IDs

The system is also somewhat fragile in the sense if the mapping.json file ever gets edited or lost. This can be mitigated somewhat in the future by allowing a regeneration of the mapping.json file by looking through all the XML description files. It's also somewhat mitigated due to this file being checked into git.

Another quirk is that there are 8 possible states to check for the existence of a testcase ID:

annotation xml mapping Action(s) Name
0 0 0 Make import request NONE
0 0 1 Edit the XML file, add to badFunction MAP
0 1 0 Edit the Mapping file, add to badFunction XML
0 1 1 Verify equality, add to badFunction XML_MAP
1 0 0 Edit the XML and mapping file ANN
1 0 1 Verify equality, edit the XML ANN_MAP
1 1 0 Verify equality, add to mapping ANN_XML
1 1 1 Verify equality ALL

Notice in the table above under the Action(s) column something that says "add to badFunction". This means that the ID existed in either the XML file or the mapping file, but not in the annotation. Ideally, this information should always be put into the source annotation, but it is not possible to rewrite code in an annotation process (you can generate new code based on an annotation, but you can't edit existing code).

To help ease the problem where the ID might exist in the XML or mapping file, but not in the annotation, everytime the code is compiled or the JarHelper main is called, a file /tmp/bad-functions.txt is created which will list the bad functions like this:

~/P/testpolarize โฏโฏโฏ cat /tmp/bad-functions.txt                                                                                                           master โœฑ
For com.github.redhatqe.rhsm.testpolarize.TestReq.testBadProjectToTestCaseID, in project PLATTP, the testCaseID is an empty string even though the corresponding XML file is present and has ID = PLATTP-10202
For com.github.redhatqe.rhsm.testpolarize.TestReq.testUpgrade, in project PLATTP, the testCaseID is an empty string even though the corresponding XML file is present and has ID = PLATTP-10068
For com.github.redhatqe.rhsm.testpolarize.TestReq.testError, in project PLATTP, the testCaseID is an empty string even though the corresponding XML file is present and has ID = PLATTP-10203
For com.github.redhatqe.rhsm.testpolarize.TestPolarize.testMethod, in project PLATTP, the testCaseID is an empty string even though the corresponding XML file is present and has ID = PLATTP-10069

What to do if there is a mismatch?

Also, there's the thorny problem of what to do if the testcase ID's don't match in one or more the above entities. For example, what if someone accidentally edits an annotation so that it no longer matches what is in the XML or in the mapping file? One of the entities should be the authoritative source, but which one? An argument could be made for why any of the 3 should be the authoritative version. However, polarize has made the choice to make the annotation as the authoritative source.

But even then, what to do if there is a mismatch? If the annotations says a testmethod maps to RHEL6-23456, but the (matching) XML definition for that method maps to RHEL6-23457, what to do? Currently, there is not a query mechanism in place, so we can not yet query the two ID's and see which one to pick. In this case, automation can not and should not automatically resolve these conflicts anymore than git merge can automatically resolve source conflicts. A human must intervene. However, polarize will at least fail on the compile if there is a mismatch on the "verify equality" check so that this intervention can take place.

Setting the update field

Over time, you might want to update one or more fields of the annotation to update the matching TestCase definition in Polarion. You can set a field called update=true and edit any of the fields to do so. The problem is that if you later forget to unset the update field, every time the code is compiled, it will make a new import request. This is a waste of resources and a burden on the polarion server.

Building polarize

Building polarize is relatively straight forward and uses gradle. The tricky part is that polarize is meant to be uploaded to maven central so that other teams may make use of it.

The project also makes use of SNAPSHOT repositories. If the version contains the word SNAPSHOT in it in the build file, then when you publish the polarize artifacts, it will go into maven's snapshot repository. If your project uses maven, gradle or leiningen, you may need to add the snapshot repository to your project.

When building a new version of polarize, all you need to do is edit the version variable in the build.gradle file. Then you can run the following sequence of commands from your polarize directory

gradle clean
gradle publish
gradle pP

The gradle clean command will remove the temporary build directory. The gradle publish command will build a maven pom file, and store that in a local artifacts directory. The gradle pP command is shorthand for the task gradle preparePublish. Why run a command called preparePublish after the publish task? The first gradle publish command is actually just to generate the maven pom and store the artifacts locally. When you are ready to upload the artifacts to maven central, then you need to run the gradle publish a second time. The build.gradle script checks for the existence of the maven pom file. If it doesn't exist, it will create the pom and store the artifacts locally. If the pom file does exist, then the publish command will do some other work including signing the all the jar files (source jars, doc jars, and the pom itself) and then upload to maven.

However, if you are in a development cycle, and you do not wish to bump to a new version and simply build the lateset snapshot, then you can just run these commands:

gradle clean
gradle pP
gradle install

This will clean your temp builds, compile everything, and install to your local maven repo (somewhere in ~/.m2/repos). If you have other projects relying on polarize, the gradle install will put the artifacts in your local maven repo.

Uploading to maven central

When you are ready to make a release version (ie, a version that doesn't have SNAPSHOT in the version name) you will need to have a few things set up. In your ~/.gradle/gradle.properties file, include the following:

  • signing.keyId= id of your pgp key
  • signing.password= the password you used for your pgp key
  • signing.secretKeyRingFile=/home/stoner/.gnupg/secring.gpg

Gradle uses this information to know how to sign all the artifacts. This is a requirement for maven central, so you must have a pgp/gpg key for the signing, and the oss central site must have your public key registered to a public site.

It is necessary to provide credentials for login to 'oss.sonatype.org' in your ~/.gradle/gradle.properties file:

  • ossrhUsername=USERNAME
  • ossrhPassword=PASSWORD

For more information on getting set up with Sonatype, please read their page:

http://central.sonatype.org/pages/ossrh-guide.html

Logging into Nexus

Log into https://oss.sonatype.org#welcome

Enter with your username and password. Click on the [Staging Repositories][-nexus] link. Scroll down until you see your project. Click on Close to close the stage if all the files look good, otherwise edit, and run gradle publish again.

When you close, give it a minute or so and then click the refresh button. If everything went well, you should see a Release button. Click that to release.

It takes several minutes or even hours for the oss repo to mirror to maven central. You can add this repo.

If you are using leiningen for your builds, you can add the maven central repo like this:

:repositories [["clojars.org" {:url       "http://clojars.org/repo"
                                 :snapshots {:update :always}}]
               ["sonatype"    {:url        "http://oss.sonatype.org/content/repositories/releases"
                              ;; If a repository contains releases only setting
                              ;; :snapshots to false will speed up dependencies.
                              :snapshots     false
                              ;; Disable signing releases deployed to this repo.
                              ;; (Not recommended.)
                              :sign-releases true
                              ;; You can also set the policies for how to handle
                              ;; :checksum failures to :fail, :warn, or :ignore.
                              :checksum      :fail
                              ;; How often should this repository be checked for
                              ;; snapshot updates? (:daily, :always, or :never)
                              :update        :always}]
                ["sonatype-snaps"
                             {:url     "https://oss.sonatype.org/content/repositories/snapshots"
                              ;; If a repository contains releases only setting
                              ;; :snapshots to false will speed up dependencies.
                              :snapshots     true
                              ;; Disable signing releases deployed to this repo.
                              ;; (Not recommended.)
                              :sign-releases false
                              ;; You can also set the policies for how to handle
                              ;; :checksum failures to :fail, :warn, or :ignore.
                              :checksum      :warn
                              ;; How often should this repository be checked for
                              ;; snapshot updates? (:daily, :always, or :never)
                              :update        :always}]
                 ]

Other metadata mechanisms for other languages

The idea of using a language's built in feature for metadata is desirable. One could try to write a language agnostic way to capture metadata, for example reading python's docstings or java's javadocs, but you still have a language specific feature (the parser would have have to recognize what a docstring was for a python method, or a javadoc). Also comments are usually meant for that, comments, not metadata. Java annotations are actually exactly the use case for adding metadata about a test. If I am a user of a library, I do not want to read in my javadocs or sphinx generated docs about some Test Case Repository system...I want to know what the functions or classes do.

So while polarize follows this idea, the majority of test teams use either python or bash, and not a few use ruby.
Other teams might be using javascript or some other language. So then, how do you create metadata for these other languages?

Target representation

As of now, polarize generates XML based on the annotation data. This is because the TestCase Importer made by the CI Ops team expects an XML file in order to generate the Polarion TestCase Workitem. In other words, XML is just a middle man. It should be possible to store the metadata in other forms like json

Python

Probably the best way to do this is to create decorators which in turn generate XML. Ironically, the syntax for python decorators could look a lot like the java version. For example, imagine something like this:

from functools import wraps

base_path = get_base_from_config()
mapping_path = get_mapping_from_config()

def test_definition(**kwargs):
    kwargs["project"] = kwargs.get("project", "RHEL6")
    kwargs["test_case_id"] = kwargs.get("test_case_id")
    kwargs["importance"] = kwargs.get("importance", "MEDIUM")
    kwargs["posneg"] = kwargs.get("posneg", "POSITIVE") 
    kwargs["level"] = kwargs.get("level", "SYSTEM")
    kwargs["linked_work_items"] = kwargs.get("linked_work_items")
    kwargs["testtype"] = kwargs.get("testtype")
    kwargs["setup"] = kwargs.get("setup", "")
    kwargs["teardown"] = kwargs.get("teardown", "")
    kwargs["tags"] = kwargs.get("tags")
    kwargs["update"] = kwargs.get("update", False)
    kwargs["automation"] = kwargs.get("automation")

    # Set up the defaults 
    test_case_id = kwargs["test_case_id"]
    update = kwargs["update"]
    if kwargs["testtype"] is None:
        # set the default
        pass

    def wrapper(fn):
        # Check to see if the XML description file for this method exists
        # If it doesn't or update == true, regenerate the XML
        # For performance, you can have a global setting to always disable this if desired
        xml_path = get_xml_path(fn.__name__, base_path)
        if update or does_xml_exist(xml_path):
            generate_xml(kwargs)
            
        # Check the test_case_id, xml file, and mapping.json.  Do what needs to be done based on the 8 possibilities
        do_import = get_test_case_id_from_all(xml_path, mapping_path, test_case_id)
        if do_import:
            test_case_id = import_request(xml_path)
            # edit the XML and mapping.json with the new test_case_id
            edit_xml(xml_path, test_case_id)
            edit_mapping(mapping_path, test_case_id)
            

        @wraps
        def processor(*args, **kwds):
            # Actually run our test method
            return fn(*args, **kwds)
        return processor
    return wrapper


@test_definition(project="RedHatEnterpriseLinux7",
                 test_case_id="",
                 importance="HIGH",
                 posneg="NEGATIVE")
def my_test_method():
    pass

Javascript

Javascript doesn't have syntactic sugar like python does for decorators, but all python decorators are, is a syntax convenience which is a higher order function that takes another function (and some additional parameters) and returns a modified function.

const test_definition = (test_def, wrap_fn) => {
    // 1. Set defaults for test_def 
    // 2. 
    return wrap_fn;
}

function my_test_method() {
    // do your test
}

var defs = {"project": "RHEL6", 
            "importance": "HIGH", 
            "level": "COMPONENT"}
const my_test_method = test_definition(defs, my_test_method)

Ruby

Unfortunately, I don't know ruby so I can't give an example, however, since ruby has the concept of higher order functions, it can follow the same strategy as javascript

polarize's People

Contributors

aftermath avatar jstavel avatar rarebreed avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

polarize's Issues

Split out various classes and functionality into their own library

right now, polarize isn't very modular. For example, the Configurator, XMLConfig and Opts classes should really all be in their own library to handle configuration settings. Also the XUnitReporter to generate xunit reports and the ImportRequest and CIBusMessageListener could be spun out into their classes so that users could make use of them without having to download all of the functionality of polarize. If you think about it, polarize has several related tasks:

  • Configuration handling
  • Import requests
  • Annotation Processing

The heart of polarize is of course the annotation processing, but it needs a lot of setup/configuration information. Indeed, some of the configuration can and probably should be handled by dependency injection. The Import Requests (both for the TestCase and XUnit Importers) share a lot of code...for example listening for a message on the bus. Also, the library could be expanded to to publish messages instead of just subscribing to a topic and listening for messages.

Running lein compile or using runtime reflection to create/update clojure test annotations deletes or edits existing entries in the mapping.json

I noticed that if I ran a lein compile, which generates mappings for the rhsm.cli.tests, and then followed that by running the runtime reflection code via:

java -cp $CP -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5009 com.github.redhatqe.polarize.JarHelper \
--jar file:///home/stoner/clean/rhsm-qe/target/sm-1.1.0-SNAPSHOT-standalone.jar \
--packages "rhsm.gui.tests,rhsm.cli.tests"

Then the mapping.json file gets changed drastically. Need to review how the runtime reflection code is different. I tried to make the runtime code use as much as possible with the annotation/compile-time code (which is why a lot of the functions in TestDefinitionProcessor are static), but there are some differences

Write a python annotation system

Polarize probably will not be widely adopted unless there is a similar system for python projects. In other words, what the annotations do for java code, there should be something similar for python projects. While at first glance it seems like a docstring parser would be a good place to put this metadata, I think this is not a good idea. The problem is that docstrings are already used for a purpose: to describe functionality.

A better idea will be to use a python decorator. This decorator would take all the data needed and generate an XML file like the TestDefinitionProcessor does.

Make logging write to a file

Currently, the logger only goes to the stdout but not to a file. This makes it difficult to do debugging on development machines.

I may also switch out to using java.util.logging instead of slf4j since polarize gets these errors about other libs pulling in slf4j (and rhsm-qe also uses a slf4j component).

If using the linkedWorkItem field, if you only specify one item to link to, but have 2 project, both projects will associate to the linked work item

The problem is if you have two projectID, like RHEL6 and RedHatEnterpriseLinux7 but you only specify one item to link to, both projects will get linked. For example:

	@TestDefinition( projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7}
	                , testCaseID = {"RHEL6-47815", "RHEL7-88467"}
	                , importance= DefTypes.Importance.MEDIUM
			, tags="tier1 polarize"
			, testtype = @TestType( testtype = DefTypes.TestTypes.FUNCTIONAL
	                                      , subtype1 = DefTypes.Subtypes.RELIABILITY
	                                      , subtype2 = DefTypes.Subtypes.EMPTY)
			, linkedWorkItems={@LinkedItem( workitemId="RHEL6-26658"
			                              , project=Project.RHEL6
			                              , role=DefTypes.Role.VERIFIES)}
			, update=true)
	@Test(description="Polarion video training",
	      groups={"polarize"})
        public void polarizeTraining3_Test() {

	}

In this example, we only want to link to the RHEL6 project, as our workitemId is RHEL6-26658. But, when we look at the XML generated by this annotation, we see that the XML description for the RedHatEnterpriseLinux7 project also gets it:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<testcases project-id="RedHatEnterpriseLinux7">
    <response-properties>
        <response-property name="rhsm_qe" value="testcase_importer"/>
    </response-properties>
    <testcase id="RHEL7-88467">
        <title>RHSM-TC : rhsm.cli.tests.ActivationKeyTests.polarizeTraining3_Test</title>
        <description>Polarion video training</description>
        <test-steps>
            <test-step>
                <test-step-column id="step"/>
            </test-step>
        </test-steps>
        <custom-fields>
            <custom-field id="caseautomation" content="automated"/>
            <custom-field id="caseimportance" content="medium"/>
            <custom-field id="caselevel" content="system"/>
            <custom-field id="caseposneg" content="positive"/>
            <custom-field id="tags" content="tier1 polarize"/>
            <custom-field id="testtype" content="functional"/>
            <custom-field id="subtype1" content="reliability"/>
            <custom-field id="subtype2" content="-"/>
        </custom-fields>
        <linked-work-items>
            <linked-work-item workitem-id="RHEL6-26658" role-id="VERIFIES"/>
        </linked-work-items>
    </testcase>
</testcases>

So when processing the TestDefinition annotation, when we store it in the Meta we need to line them up like we do with projectID-testcaseID

Get rid of the error message about "Unknown property..." in Configurator call

If you run the Configurator.main(), you will see possibly 3 different messages:

12:53:20.727 [main] ERROR c.g.r.p.configuration.Configurator - Unknown property name testrun-title
12:53:20.727 [main] ERROR c.g.r.p.configuration.Configurator - Unknown property name testrun-id
12:53:20.727 [main] ERROR c.g.r.p.configuration.Configurator - Unknown property name testrun-template-id

It doesn't actually seem to affect anything, but the error messages are misleading. Track down how these errors are printing and remove if they are superfluous

Doubling up on generating testcases with TestCase Importer

I found several problems with the TestCase importer. The first is while doing something like this:

@TestDefinition( projectID = {Project.RHEL6, Project.RedHatEnterpriseLinux7} 
                            , testCaseID = {"", ""})
public void foo() {

}

It would generate 2 RedHatEnterpriseLinux7 TestCases instead of one.

Configurator utility needs documentation and a --help as well as general clean up

The Configurator CLI utility is very confusing. There is sort of a toggle switch --edit-config. When this is set to true, it essentially ignores any of the CLI switches dealing with setting the xunit file. And if it is set to false, it will ignore the any of the CLI switches to edit the xml-config.xml file.

Also, it's not clear what exactly all the command line switches even are. There;s no --help option.

If a method uses a dataProvider and the TestDefinition has 2 or more projects, the params only shows for one project

Here is an example of the problem:

  "rhsm.cli.tests.BugzillaTests.VerifyEUSRHELProductCertVersionFromEachCDNReleaseVersion_Test" : {
    "RHEL6" : {
      "id" : "RHEL6-26709",
      "parameters" : [ ]
    },
    "RedHatEnterpriseLinux7" : {
      "id" : "",
      "parameters" : [ "blockedByBug", "release", "rhelRepoUrl", "eusEntitlementCertFile" ]
    }
  }

The param data is picked up in RedHatEnterpriseLinux7, but not in RHEL6 even though it should be in the parameters field there too.

The updateMapping function is adding to the mapping.json even if nothing changed

The logic for when to write to the mapping file was not right. Even if the method/project was already in the mapping file with the same ID, the updateMapping file would print a message saying it was editing the mapping file. However, this is unnecessary if the ID is the same. This can happen when the annotation's testCaseID == "", but it exists in either the XML or mapping file. While this is not wrong per se, it is confusing due to the logging message, and also hurts performance by needing to unnecessarily open the mapping file and write to it.

To prevent the logging message and for performance, do not edit the mapping file if it's not necessary

Write a BDD Gherkin parser

Ideally, it would be nice to specify requirements and/or testcases in a BDD style feature format using a modified gherkin syntax. For example:

Feature: Generate XML from annotated class

  Polarion:
    severity: must_have      # One of should_have, must_have, nice_to_have, will_not_have
    type: Functional         # One of Functional, NonFunctional,
    subType:                 # optional
    status: draft            # defaults to draft
    title:                   # Not filled in by PM.  Done by QE
    plannedin                # a list of plannedin releases
      - RHEL_7_3
    links
      - /some/link/to/share  # optional


  Scenario: Annotated class generates valid XML
    Given The class is annotated with @Polarion
      And The Polarion annotation has author
      And The Polarion annotation has projectId
      And The Polarion annotation has sub-annotation Requirement
      And The field for xmlDesc is an empty string in the Annotation
     Then XML suitable for the WorkItem importer will be generated

This is for a requirement, but similar syntax could be done for a testcase. The parser will read this file in, and extract the relevant information. This relevant information would then be used to generate the XML

Only the first testCaseID is populated in the mapping.json even if the annotation has for 2 or more

Here is an example of the problem. This json snippet is produced...

  "rhsm.cli.tests.BugzillaTests.VerifyAutohealForSubscription" : {
    "RHEL6" : {
      "id" : "RHEL6-21937",
      "parameters" : [ ]
    },
    "RedHatEnterpriseLinux7" : {
      "id" : "",
      "parameters" : [ ]
    }
  }

Given this annotation:

@TestDefinition( projectID = {Project.RHEL6, Project.RedHatEnterpriseLinux7}
	                   , testCaseID = {"RHEL6-26709", "RHEL7-63527"})
@Test(description = "Verify that the EUS RHEL product certs on the CDN for each release correctly reflect the release version.  For example, this affects users that want use subcription-manager release --set=6.3 to keep yum updates fixed to an older release.", groups = {
			"VerifyEUSRHELProductCertVersionFromEachCDNReleaseVersion_Test", "AcceptanceTests",
			"Tier1Tests" }, dataProvider = "VerifyEUSRHELProductCertVersionFromEachCDNReleaseVersion_TestData", enabled = true)
	public void VerifyEUSRHELProductCertVersionFromEachCDNReleaseVersion_Test(Object blockedByBug, String release,

Make the testcase importer work across projects

The current way that the testcaseImporterRequest() function works is to only submit an import request for whatever project is defined in the config file. However, that is a problem if there are multiple projects that need to be created for a new method.

The dev branch has code to implement this, but it can only be tested if I can find another project other than PLATTP.

When update=true, but testCaseID is "", even if there is an existing XML file with the ID in it, a new Import Request is made with empty ID

If update=true, and the testCaseID is "" in the annotation, even if the XML for this method exists and there is an ID from the already existing testcase in it, a new request is performed.

This is because when update=true, it only looks at the annotation metadata, not at the existing XML. Since the annotation still has an empty string for the testCaseID, the XML that will get passed to the TestCase Importer will also have an empty string.

This has to be done because let's say you wanted to change importance from HIGH to MEDIUM. You set this in the annotation. The already existing XML already has caseimporter set to HIGH. Since you set update=true, it replaces everything in your annotation...basically the annotation data wins. Since the testCaseID is still "" because the author didn't go back and fill it in, this "" for the testCaseID "wins" and the XML will have an empty ID again.

Workaround for PJA-99

In order for data driven methods to properly report results to Polarion, the TestCase must have in the TestSteps field the parameters defined. If you do this manually, you can go to the TestSteps field, and then with the mouse, click in the step. You should see a box pop up asking if you want to add a parameter.

The automation does this by adding to the XML something like below:

        <test-step>
            <test-step-column id="step">
                <parameter name="blockedByBug" scope="local"/>
                <parameter name="keyName" scope="local"/>
                <parameter name="jsonPool" scope="local"/>
            </test-step-column>
        </test-step>

The TestCase that belongs to this will have the parameters added. The problem is that the TestCase importer replaces everything, so we need to know what the original values of all fields are, not just the ones we want to change

Test the scenario where one Project has a test case ID but another Project is blank

Need to test a scenario like this:

@TestDefinition( projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7}
                            , testCaseID={"RHEL6-27812"})

Actually, the above scenario should fail, because the length or the projectID array doesn't match the length of the testCaseID. The correct scenario would actually be

@TestDefinition( projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7}
                            , testCaseID={"RHEL6-27812", ""})

Reusing a single Testcases object for multiple project leads to testcases that shouldn't be in the XML

Now that I am actually annotating the rhsm-qe code and using the real polarion server, I am able to actually use 2 different project IDs. For the devel polarion, I only had access to one Project ID (PLATTP) so I was unable to test the scenario if a function was annotated with multiple projects. For example:

@TestDefinition( projectID={Project.RHEL6, Project.RedHatEnterpriseLinux7} )
@Test(    description="assert that brandbot service is running",
            groups={"AcceptanceTests","Tier1Tests"},
            enabled=false)    // TODO not sure how this works... the status of this service is inactive, yet it appears to be automatically started/stopped as needed NEEDINFO from notting
    public void BrandbotServiceShouldBeRunning_Test() {
        if (Integer.valueOf(clienttasks.redhatReleaseX)<7) throw new SkipException("Brandbot is an initscripts tool feature of Flexible Branding in RHEL7.");    // initscripts-9.49.17-1.el7
        RemoteFileTasks.runCommandAndAssert(client, "systemctl is-active brandbot.service", Integer.valueOf(0), "^active$", null);
    }

The problem is that I am passing in a member of TestDefinitionProcessor.testcases for the call to both the RHEL6 and RedHatEnterpriseLinux7 projects. Because it is using the same Testcases object for every project when it iterates over all the projects in the tcMap, it results in a later project having the elements of earlier projects in the XML file. This can result in creating new Polarion TestCases for already existing test methods.

I need to create two different testcase objects, one for each project. Something like:

//private Testcases testcases = new Testcases();
private Map<String, TestCases> testcases = new HashMap<>();

Create a java Annotation Processor to supply metadata about TestCase and Requirements

Also see RedHatQE/pong#23

In order to track what TestCase and Requirement a test method fulfills, we need to add some annotation types and annotate the testmethods with it. Currently, pong uses the class.method_name as an ID for the TestCase and Requirement. However, this is brittle. When the requirement to prefix a TestCase with "RHSM-TC" was made, a query for an exact match failed to detect "rhsm.cli.test.mytest.foo" which already existed. If OTOH we only did a query for rhsm.cli.test.mytest.foo, due to the nature of lucene queries, it would return 2 matches: rhsm.cli.test.mytest.foo and "RHSM-TC rhsm.cli.test.mytest.foo". So what we need to do is map the unique Polarion workitem ID as metadata for the test method

We need to cover 2 scenarios:

  1. A TestCase and Requirement already exist for the test method
  2. The TestCase and/or Requirement don't exist for the test method

To cover these scenarios, we should create something like the following:

public enum CaseType {
    POSITIVE, NEGATIVE
}

public enum Importance {
    LOW, MEDIUM, HIGH
}

public @interface Polarions {
    public Polarion[] value();
}

@Repeatable(Polarions.class)
public @interface Polarion {
    public String xmlConfig();            // path to xml file which will override all values if it exists
    public String projectID();
    public String testCaseID();
    public String[] requirementIDs();  // a : separated string of ID:feature-file-name
    public CaseType caseType();      // CaseType is an enum
    public String component();
    public Importance importance();
}

In scenario 1, if the work items already exist, we can annotate a test like this:

@Polarion(projectID="RHEL6",
           testCaseID="RHEL6-25678",
           requirementsID={"RHEL6-24567:reqs/already-subscribed.feature"},
           caseType=POSITIVE,
           component="subscription-manager",
           importance=HIGH)
 @Polarion(projectID="RedHatEnterpriseLinux7",
           testCaseID="RHEL7-32456",
           requirementsID={"RHEL7-31456:reqs/already-subscribed.feature"},
           caseType=POSITIVE,
           component="subscription-manager",
           importance=HIGH)
 @Test(description="Subscribed for Already subscribed Entitlement.",
       groups={"blockedByBug-584137","blockedByBug-979492"},
       // dataProvider="getAvailableSubscriptionPoolsData", TAKES TOO LONG AND RARELY REVEALS A BUG - changing to getRandomSubsetOfAvailableSubscriptionPoolsData
       dataProvider="getRandomSubsetOfAvailableSubscriptionPoolsData",
       enabled=true)
 @ImplementsNitrateTest(caseId=41897)
 public void AttemptToSubscribeToAnAlreadySubscribedPool_Test(SubscriptionPool pool) throws JSONException, Exception{
   String consumerId = clienttasks.getCurrentConsumerId();
   Assert.assertNull(CandlepinTasks.getConsumersNewestEntitlementSerialCorrespondingToSubscribedPoolId(sm_clientUsername, sm_clientPassword, sm_serverUrl, consumerId, pool.poolId),"The current consumer has not been granted any entitlements from pool '"+pool.poolId+"'.");
   Assert.assertNotNull(clienttasks.subscribeToSubscriptionPool_(pool),"Authenticator '"+sm_clientUsername+"' has been granted an entitlement from pool '"+pool.poolId+"' under organization '"+sm_clientOrg+"'.");
   // ...
}

In scenario 2, if the workitems do not yet exist, then we can mark them as empty strings. If they are empty or null, then the annotation processor needs to generate an XML file that can be handed off to the WorkItem exporter. When the WorkItem exporter returns the response, the code will use that.

@Polarion(projectID="RHEL6",
          testCaseID="",
          requirementsID={},
          caseType=POSITIVE,
          component="subscription-manager",
          importance=HIGH)
@Test()
public void foo() {
    // ...
}

Take out or make the check for description in TestDefinition

After I found out that the description wasn't in every XML description file, I wrote a check to make sure the description was in the XML file.

It appears that this check always returns true, and the XML is checked and the description is added again. Fix the flaw in the logic that causes this check to always be true.

Add JSON parsing so that we can add this as a REST based microservice

I already have a JSON based version of the config file:

{
     "base-dir": "",
     "author": "stoner",
     "mapping": "{basedir}/mapping.json",
     "testcases-xml": "{basedir}/testcases",
     "project": "",
     "project-name": "",
     "group-id": "",
     "edit-config": "",
     "testcase": {
         "prefix": "",
         "suffix": "",
         "importer-enabled": false,
         "timeout": 300000,
         "file": "/tmp/testcases.xml",
         "selector-name": "rhsm_qe",
         "selector-val": "testcase_importer"
     },
     "xunit": {
         "new": "/tmp/modified-testng-polarion.xml",
         "file": "{base-dir}/test-output/testng-polarion.xml",
         "importer-enabled": true,
         "timeout": 3000000,
         "selector-name": "rhsm_qe",
         "selector-val": "xunit_importer"
     },
     "testrun": {
         "template-id": "sean toner master test template",
         "title": "RHSM TestRun for RHEL 7.4 Server x86_64",
         "id": "RHSM TestRun for RHEL 7.4 Server x86_64 NOV-8-2016-14-32-10",
         "dry-run": false,
         "set-finished": true,
         "include-skipped": true
     },
     "properties": [
         {
             "name": "plannedin",
             "val": "RHEL_7_4 Planning"
         },
         {
             "name": "jenkinsjobs",
             "val": "http://path/to/jenkins/job"
         }
     ],
     "servers": [
         {
             "name": "polarion",
             "user": "ci-user",
             "password": "password",
             "url": "http://path/to/polarion"
         },
         {
             "name": "kerb",
             "user": "user",
             "password": "password"
         }
     ]
}

I can marshall this JSON into an Opts class already. What needs to be done now is essentially convert the Opts object to its equivalent Config object and then marshall that

Project ID not being passed to the testcaseImportHandler correctly

I see a problem where the Polarion ID exists in the mapping.json file, but not in the XML file for that method->project. I wrote some code to handle this situation, but it still does not appear to be working correctly, because what happens is that since the XML file has an empty string, a new request is made by the ImportRequest.

Worse, even though the project should be PLATTP, for some reason, in the importer it is making a request to the RHEL6 project.

From stepping through the debugger, it appears that I am not passing in the correct project to the testCaseImportHandler(). I had made a change where the test case importer will cycle through each project (eg PLATTP, RHEL6, etc) and make one import request per project. However, I didn't not also change the testCaseImportHandler to use the new project, it instead uses whatever project is specified by the xml-config file.

So, I need to pass along the project information to the testCaseImportHandler function.

The Configurator utility crashes with NPE if using --testcase-prefix or --testcase-suffix

If you try to use this option like so:

java -cp ${POLARIZE_JAR} com.github.redhatqe.polarize.configuration.Configurator /tmp/xml-config.xml --project RHEL6 --template-id "Sean Toner Test Template" --testrun-title "RHSM Polarize Test" --xunit-selector-name rhsm_qe --xunit-selector-val polarize  --testcase-importer-enabled false  --property plannedin="RHEL_7_4 Planning" --property arch=x8664 --new-xunit /tmp/mod-testng-polarion.xml --current-xunit /tmp/testng-polarion.xml --base-dir /home/stoner/Projects/testpolarize --property notes="just a test" --edit-config true --testcase-prefix "No TC : " --testcase-suffix "Server blach"

It will throw a null pointer exception. This is because the code is looking for prefix and suffix in the "xunit" importer type instead of the "testcase" importer.

Null Pointer Exception while running Tier1 CLI suite

I ran a tier1 CLI suite overnight and it hit an NPE. It appears to be in the setTestSuiteResults method. I believe the problem is that the FullResult object is null.

Here is the relevant code:

       XmlTest xt = ctx.getCurrentXmlTest();
       List<XmlClass> clses = xt.getClasses();
       Set<String> clsSet = clses.stream()
               .map(x -> {
                    String sptCls = x.getSupportClass().toString();
                    sptCls = sptCls.replace("class ", "");
                    return sptCls;
               })
               .collect(Collectors.toSet());

        //..
        // While I suppose it's possible, we should have only one or zero possible results 
        // from the map, so findFirst should return at most 1.  When will we have zero?
        List<Tuple<FullResult, List<Testcase>>> frList = full.entrySet().stream()
                .filter(e -> {
                    String cls = e.getKey();
                    return clsSet.contains(cls);
                })
                .map(Map.Entry::getValue)
                .collect(Collectors.toList());
        Optional<Tuple<FullResult, List<Testcase>>> maybeFR = frList.stream().findFirst();
        Tuple<FullResult, List<Testcase>> tup = maybeFR.orElse(new Tuple<>());
        FullResult fr = tup.first;
        List<Testcase> tcs = tup.second;
        if (tcs != null)
            tests.addAll(tcs);

        setTestSuiteResults(ts, fr, ctx);

I believe we can get an NPE in the following scenarios:

The clsSet is empty. If it is empty, then frList is empty. When frList is empty, the Optional is empty, so we use the default Tuple constructor. This should only happen if clses is empty somehow.

The filter for the frList returns no matches. If there are no matches, frList is empty with the same result as above. So when is the filter on frList going to return no matches?

  1. If full is empty there will be no matches
  2. If none of the keys in full (which are class names) are in clsSet

Finally, frList could return the first element, but the FullResult is null. The getMethodInfo method is creating a map of String -> Tuple<FullResult, List>. It is also possible that FullResult could be null if it mapped a class name to a null FullResult.

Add stack exceptions to the xunit result file

After talking with the logstash team, it won't be necessary to include the stdout/stderr of our job since they have a console monitor already. However, including the stacktrace will make it easier to isolate just the exceptions.

Need to grab the stacktrace from TestNG

Reflector code needs to look for methods with multiple @TestDefinitions

The Reflector code currently only gets annotations for TestDefinition.class, not TestDefinitions.class (notice the plural). That means if a method is annotated twice, which can happen if the definition differs between Projects for a method, that the Reflector code won't find this method.

Add code to process methods using repeating @TestDefinition

Make the error reporting better

Check your @TestDefinition for a missing testCaseID corresponding to projectID RedHatEnterpriseLinux7 in rhsm.cli.tests.GeneralTests.MyFirstPolarize_Test

Add this also:

To create a new testcase in Polarion and retrieve its testCaseID, set <enabled>true</enabled> under the <importer type="testcase"> section of your xml-config.xml file.

Also put this information in /tmp/polarize-errors.txt instead of /tmp/bad-functions.txt

After generateReport is called, the xunit style differs from the old TestNG way of reporting

I ran the full sm-cli-acceptance suite last night and once it completed the generated XML file was huge (72 MB). Also, the name of the test suite and the other attributes were wrong:

 <testsuite name="CLI: Help Tests" tests="0" failures="0" time="0.0" skipped="0">

Clearly, that isn't correct.

Worse, when this xunit file is POST'ed, it doesn't seem to go through. Or rather, there is no record of the file being processed by the XUnit Importer plugin in polarion.

TestCase Importer update=true fails. The testcases.xml has both the RHEL6, and RedHatEnterpriseLinux7 values when there should be two

When running an update on both a RHEL6 and RedHatEnterpriseLinux7, it generates the modified XML for each method for each project. it updates RHEL6 successfully (if testCaseID is not "" in the annotation). But for RedHatEnterpriseLinux7 it generates a testcase.xml like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<testcases project-id="RedHatEnterpriseLinux7">
    <response-properties>
        <response-property name="rhsm_qe" value="testcase_importer"/>
    </response-properties>
    <testcase id="RHEL6-47808">
        <title>RHSM-TC : rhsm.cli.tests.ActivationKeyTests.polarizeTraining_Test</title>
        <description>Training for </description>
        <test-steps>
            <test-step>
                <test-step-column id="step"/>
            </test-step>
        </test-steps>
        <custom-fields>
            <custom-field id="caseautomation" content="automated"/>
            <custom-field id="caseimportance" content="medium"/>
            <custom-field id="caselevel" content="system"/>
            <custom-field id="caseposneg" content="positive"/>
            <custom-field id="testtype" content="functional"/>
            <custom-field id="subtype1" content="performance"/>
            <custom-field id="subtype2" content="-"/>
        </custom-fields>
    </testcase>
    <testcase id="RHEL7-88279">
        <title>RHSM-TC : rhsm.cli.tests.ActivationKeyTests.polarizeTraining_Test</title>
        <description>Training for </description>
        <test-steps>
            <test-step>
                <test-step-column id="step"/>
            </test-step>
        </test-steps>
        <custom-fields>
            <custom-field id="caseautomation" content="automated"/>
            <custom-field id="caseimportance" content="medium"/>
            <custom-field id="caselevel" content="system"/>
            <custom-field id="caseposneg" content="positive"/>
            <custom-field id="testtype" content="functional"/>
            <custom-field id="subtype1" content="performance"/>
            <custom-field id="subtype2" content="-"/>
        </custom-fields>
    </testcase>
</testcases>

Because it has both a RHEL6 and a RedHatEnterpriseLinux7 ID, the message response looks like this:

{
  "import-testcases" : [ {
    "name" : "RHSM-TC : rhsm.cli.tests.ActivationKeyTests.polarizeTraining_Test",
    "status" : "failed",
    "id" : "RHEL6-47808",
    "message" : "Unhandled exception processing test case.\ncom.polarion.platform.persistence.UnresolvableObjectException: uri: subterra:data-service:objects:/default/RedHatEnterpriseLinux7${WorkItem}RHEL6-47808\n\tat com.polarion.platform.persistence.spi.PObject.checkResolved(PObject.java:590)\n\tat com.polarion.platform.persistence.spi.PObject.setValue(PObject.java:527)\n\tat com.polarion.alm.tracker.internal.model.WorkflowObject.setValue(WorkflowObject.java:144)\n\tat com.polarion.alm.tracker.internal.model.WorkItem.setValue(WorkItem.java:1490)\n\tat com.polarion.alm.tracker.internal.model.WorkItem.setTitle(WorkItem.java:1410)\n\tat com.redhat.importer.testcase.ImportTestcaseThread.processTestcase(ImportTestcaseThread.java:264)\n\tat com.redhat.importer.testcase.ImportTestcaseThread.process(ImportTestcaseThread.java:131)\n\tat com.redhat.importer.base.RedHatWorkerThread.importAs(RedHatWorkerThread.java:257)\n\tat com.redhat.importer.base.RedHatWorkerThread$1.run(RedHatWorkerThread.java:236)\n\tat java.security.AccessController.doPrivileged(Native Method)\n\tat javax.security.auth.Subject.doAs(Subject.java:360)\n\tat com.polarion.platform.internal.security.SubjectNDC.doAs(SubjectNDC.java:58)\n\tat com.polarion.platform.internal.security.SecurityService.doAsUser(SecurityService.java:414)\n\tat $ISecurityService_15b1558c316.doAsUser($ISecurityService_15b1558c316.java)\n\tat com.redhat.importer.base.RedHatWorkerThread.run(RedHatWorkerThread.java:243)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\n\tat java.lang.Thread.run(Thread.java:745)\n"
  }, {
    "name" : "RHSM-TC : rhsm.cli.tests.ActivationKeyTests.polarizeTraining_Test",
    "status" : "passed",
    "id" : "RHEL7-88279"
  } ],
  "status" : "partial",
  "log-url" : "http://ops-qe-logstash-2.rhev-ci-vms.eng.rdu2.redhat.com:9981/polarion/RedHatEnterpriseLinux7/20170405-084841.183.log"
}

I believe this issue is related to the doubling up problem in #43

Add a new configuration option to set the TestRun type

There is a column called Type for a TestRun. In the xml that gets posted to the XUnit Importer endpoint, it can take a value like this:

<property name="polarion-testrun-type-id " val="..."/>

It looks like from Polarion, it's a drop down box with three choices, so this should probably be an enum of strings. Since these are Polarion properties, the code needs to add a new type TESTRUN_TYPE into the Opts type, and add it to any method that has a switch on the property types

Don't read in XML description file unless necessary

In the processIdentities method, for every method, it will open and read in the matching XML description file to check that the Polarion ID is there. This already takes close to half a minute for a thousand methods because of all the File IO. If polarize is used by teams with tens of thousands of methods, this could add several minutes to the compile time.

Instead of always loading the XML file, check the annotation first. If it does not exist there, then check the mapping file. Only then check the XML description file

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.