Skip to content

Commit

Permalink
Add a read/write test, and a stub identifier test.
Browse files Browse the repository at this point in the history
The read/write test expects to be configured (via environment variables;
see the `SerialPortExtension` Javadoc for details) with the names of two
serial ports connected to each other with a null modem cable. Armed with
this hardware, it verifies basic read/write functionality of the library
across a range of baud rates, and using both block and byte transfers.

The identifier test merely verifies a non-null response. Not terribly
useful at present, but I hope to use it to validate that the ports
configured for the other test are reported correctly (assuming that the
user configured them correctly, of course).
  • Loading branch information
MrDOS committed Jun 17, 2020
1 parent 923d8a7 commit 7c316dd
Show file tree
Hide file tree
Showing 3 changed files with 592 additions and 0 deletions.
26 changes: 26 additions & 0 deletions src/test/java/gnu/io/CommPortIdentifierTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gnu.io;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.Enumeration;

import org.junit.jupiter.api.Test;

class CommPortIdentifierTest
{
@Test
void testGetCommPortIdentifiers()
{
@SuppressWarnings("rawtypes")
Enumeration identifiers = CommPortIdentifier.getPortIdentifiers();
assertNotNull(identifiers);

/* Hard to assert on anything else when the test hardware may change
* dramatically.
*
* TODO: Wire up SerialPortExtension without its test-disabling
* ExecutionCondition behaviour, and use the names of its configured
* ports to confirm that the identifiers enumeration at least contains
* something when we know what to expect in it. */
}
}
220 changes: 220 additions & 0 deletions src/test/java/gnu/io/SerialPortExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package gnu.io;

import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Logger;

import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
* Provisioning of serial ports for functionality testing.
* <p>
* This test extension automates the acquisition and resetting of two serial
* ports for functional testing. Tests which use these ports assume that
* they're connected to each other with a null modem cable so that data can be
* exchanged between them. Then, so that this class will use them, set the
* <code>NRJS_TEST_PORT_A</code> and <code>NRJS_TEST_PORT_B</code> environment
* variables to the names of the ports prior to executing tests.
* <p>
* For example:
*
* <pre>
* NRJS_TEST_PORT_A=/dev/ttyS0 NRJS_TEST_PORT_B=/dev/ttyS1 ./gradlew test
* </pre>
*
* Or, on Windows:
*
* <pre>
* set NRJS_TEST_PORT_A=COM1
* set NRJS_TEST_PORT_B=COM2
* gradlew.bat test
* </pre>
*/
class SerialPortExtension implements Closeable, ExecutionCondition, BeforeEachCallback, AfterEachCallback
{
private static final Logger log = Logger.getLogger(SerialPortExtension.class.getName());

/** The environment variable holding the name of the first test port. */
static final String A_PORT_ENV = "NRJS_TEST_PORT_A";
/** The environment variable holding the name of the second test port. */
static final String B_PORT_ENV = "NRJS_TEST_PORT_B";

/**
* The owner name to pass to {@link CommPortIdentifier#open(String, int)}.
*/
private static final String PORT_OWNER = "NRJavaSerial - SerialPortExtension";
/** The timeout to pass to {@link CommPortIdentifier#open(String, int)}. */
private static final int OPEN_TIMEOUT = 1_000;

/**
* When the test port configuration is sane, this is returned to indicate
* that tests relying on serial ports should be enabled.
*/
private static final ConditionEvaluationResult HAS_PORTS = ConditionEvaluationResult
.enabled("Two serial ports are available, and could be opened.");
/**
* When test ports aren't configured, this is returned to indicate that
* tests relying on serial ports should be disabled.
*/
private static final ConditionEvaluationResult MISSING_PORTS = ConditionEvaluationResult
.disabled("The system does not have two serial ports, or they could not be opened.");

/** Whether both test port identifiers have been populated. */
final boolean hasIds;
/**
* The first test port identifier.
* <p>
* Populated at construction by searching for ports matching the name given
* via the {@link SerialPortExtension#A_PORT_ENV} environment variable.
*/
final CommPortIdentifier aId;
/**
* The second test port identifier.
* <p>
* Populated at construction by searching for ports matching the name given
* via the {@link SerialPortExtension#B_PORT_ENV} environment variable.
*/
final CommPortIdentifier bId;
/**
* The first test port.
* <p>
* Ports are opened from their corresponding identifiers prior to each
* test, and are closed afterwards. This field will always be null outside
* of the context of a test.
*/
SerialPort a = null;
/**
* The second test port.
* <p>
* Ports are opened from their corresponding identifiers prior to each
* test, and are closed afterwards. This field will always be null outside
* of the context of a test.
*/
SerialPort b = null;

public SerialPortExtension()
{
String aName = null;
String bName = null;
try
{
aName = System.getenv(SerialPortExtension.A_PORT_ENV);
bName = System.getenv(SerialPortExtension.B_PORT_ENV);
}
catch (SecurityException e)
{
log.severe("Failed a security check while accessing the environment variable containing the port name: "
+ e.getMessage());
}

if (aName == null || bName == null)
{
String gradlew;
String setter;
String[] ports;
if (OS.WINDOWS.isCurrentOs())
{
gradlew = "gradlew.bat";
setter = "set";
ports = new String[] { "COM1", "COM2" };
}
else if (OS.MAC.isCurrentOs())
{
gradlew = "./gradlew";
setter = "export";
ports = new String[] { "/dev/tty.usbserial-a", "/dev/tty.usbserial-b" };
}
else
{
gradlew = "./gradlew";
setter = "export";
ports = new String[] { "dev/ttyUSB0", "/dev/ttyUSB1" };
}
log.severe("The serial port functionality tests require the use of two ports. These should be connected to "
+ "each other with a null modem cable. Then set the environment variables "
+ SerialPortExtension.A_PORT_ENV + " and " + SerialPortExtension.B_PORT_ENV + " to the names of "
+ "the ports, restart the Gradle daemon, and re-run the tests. For example:\n\n\t" + setter + " "
+ SerialPortExtension.A_PORT_ENV + "=" + ports[0] + " " + SerialPortExtension.B_PORT_ENV + "="
+ ports[1] + "\n\t" + gradlew + " --stop\n\t" + gradlew + " cleanTest test --no-build-cache "
+ "--info");

this.hasIds = false;
this.aId = null;
this.bId = null;
return;
}

CommPortIdentifier aId;
CommPortIdentifier bId;
try
{
aId = CommPortIdentifier.getPortIdentifier(aName);
bId = CommPortIdentifier.getPortIdentifier(bName);
}
catch (NoSuchPortException e)
{
log.severe("No such port: " + e.getMessage());

this.hasIds = false;
this.aId = null;
this.bId = null;
return;
}

log.info("Will attempt to use ports " + aId.getName() + " and " + bId.getName() + " for serial port "
+ "functionality tests.");
this.hasIds = true;
this.aId = aId;
this.bId = bId;
}

@Override
public void close()
{
if (this.a != null)
{
a.close();
a = null;
}
if (this.b != null)
{
b.close();
b = null;
}
}

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context)
{
return this.hasIds
? SerialPortExtension.HAS_PORTS
: SerialPortExtension.MISSING_PORTS;
}

@Override
public void beforeEach(ExtensionContext context) throws PortInUseException, NoSuchPortException
{
try
{
this.a = this.aId.open(SerialPortExtension.PORT_OWNER, SerialPortExtension.OPEN_TIMEOUT);
this.b = this.bId.open(SerialPortExtension.PORT_OWNER, SerialPortExtension.OPEN_TIMEOUT);
}
catch (PortInUseException e)
{
log.severe("Port is in use: " + e.getMessage());
this.close();
throw e;
}
}

@Override
public void afterEach(ExtensionContext context) throws IOException
{
this.close();
}
}
Loading

0 comments on commit 7c316dd

Please sign in to comment.