feat: Add support for shell v2 commands (vidstige/jadb#121)

This commit is contained in:
Lucaskyy 2022-06-11 18:14:23 +02:00
parent fedca18ae1
commit 2531a28109
No known key found for this signature in database
GPG Key ID: 1530BFF96D1EEB89
9 changed files with 676 additions and 60 deletions

View File

@ -30,7 +30,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.target>1.7</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>
<mockito-core.version>2.7.17</mockito-core.version>
<mockito-core.version>2.28.2</mockito-core.version>
</properties>
<dependencies>
@ -116,7 +116,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.7.201606060606</version>
<version>0.8.4</version>
<executions>
<execution>
<goals>
@ -196,7 +196,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.3</version>
<version>2.10.4</version>
<executions>
<execution>
<id>attach-javadocs</id>

View File

@ -57,7 +57,7 @@ public class JadbDevice {
}
}
private Transport getTransport() throws IOException, JadbException {
Transport getTransport() throws IOException, JadbException {
Transport transport = transportFactory.createTransport();
// Do not use try-with-resources here. We want to return unclosed Transport and it is up to caller
// to close it. Here we close it only in case of exception.
@ -116,6 +116,19 @@ public class JadbDevice {
}
}
/** <p>Execute a shell command.</p>
*
* <p>This method supports separate stdin, stdout, and stderr streams, as well as a return code. The shell command
* is not executed until calling {@link ShellProcessBuilder#start()}, which returns a {@link Process}.</p>
*
* @param command main command to run, e.g. "screencap"
* @param args arguments to the command, e.g. "-p".
* @return a {@link ShellProcessBuilder}
*/
public ShellProcessBuilder shellProcessBuilder(String command, String... args) {
return new ShellProcessBuilder(this, buildCmdLine(command, args).toString());
}
/** <p>Execute a command with raw binary output.</p>
*
* <p>Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary

View File

@ -0,0 +1,111 @@
package se.vidstige.jadb;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.*;
public class ShellProcess extends Process {
private static final int KILLED_STATUS_CODE = 9;
private final OutputStream outputStream;
private final InputStream inputStream;
private final InputStream errorStream;
private final Future<Integer> exitCodeFuture;
private final ShellProtocolTransport transport;
private Integer exitCode = null;
ShellProcess(OutputStream outputStream, InputStream inputStream, InputStream errorStream, Future<Integer> exitCodeFuture, ShellProtocolTransport transport) {
this.outputStream = outputStream;
this.inputStream = inputStream;
this.errorStream = errorStream;
this.exitCodeFuture = exitCodeFuture;
this.transport = transport;
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
@Override
public InputStream getErrorStream() {
return errorStream;
}
@Override
public int waitFor() throws InterruptedException {
if (exitCode == null) {
try {
exitCode = exitCodeFuture.get();
} catch (CancellationException e) {
exitCode = KILLED_STATUS_CODE;
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
return exitCode;
}
/* For 1.8 */
public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
if (exitCode == null) {
try {
exitCode = exitCodeFuture.get(timeout, unit);
} catch (CancellationException e) {
exitCode = KILLED_STATUS_CODE;
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
return false;
}
}
return true;
}
@Override
public int exitValue() {
if (exitCode != null) {
return exitCode;
}
if (exitCodeFuture.isDone()) {
try {
exitCode = exitCodeFuture.get(0, TimeUnit.SECONDS);
return exitCode;
} catch (CancellationException e) {
exitCode = KILLED_STATUS_CODE;
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
// fallthrough, but should never happen
} catch (InterruptedException e) {
// fallthrough, but should never happen
Thread.currentThread().interrupt();
}
}
throw new IllegalThreadStateException();
}
@Override
public void destroy() {
if (isAlive()) {
try {
exitCodeFuture.cancel(true);
// interrupt (usually) doesn't work for blocking read -- this will cause SocketException
transport.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/* For 1.8 */
public boolean isAlive() {
return !exitCodeFuture.isDone();
}
}

View File

@ -0,0 +1,242 @@
package se.vidstige.jadb;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.*;
/**
* A builder of a {@link Process} corresponding to an ADB shell command.
*
* <p>This builder allows for configuration of the {@link Process}'s output and error streams as well as the
* {@link Executor} to use when starting the shell process. The output and error streams may be either be redirected
* (using {@link java.lang.ProcessBuilder.Redirect}) or given an explicit {@link OutputStream}. You may also combine
* the output and error streams via {@link #redirectErrorStream(boolean) redirectErrorStream(true)}.</p>
*
* <p>Use {@link #start()} to execute the command, and then use {@link Process#waitFor()} to wait for the command to
* complete.</p>
*
* <p><b>Warning:</b> If stdout and stderr are both set to {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default),
* you <b>must</b> read from their InputStreams ({@link Process#getInputStream()} and {@link Process#getErrorStream()})
* <b>concurrently</b>. This requires having two separate threads to read the input streams separately. Otherwise,
* the process may deadlock. To avoid using threads, you can use {@link #redirectErrorStream(boolean)}, in which case
* you must read all output from {@link Process#getInputStream()} before calling {@link Process#waitFor()}:
*
* <pre>{@code
* Process process = jadbDevice.shellProcessBuilder("command")
* .redirectErrorStream(errorStream)
* .start();
* String stdoutAndStderr = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
* int exitCode = process.waitFor();
* }</pre>
* <p>
* You can also use one of the {@code redirectOutput} methods to have the output automatically redirected. For example,
* to buffer all of stdout and stderr separately, you can use {@link java.io.ByteArrayOutputStream}:
*
* <pre>{@code
* ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
* ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
* Process process = jadbDevice.shellProcessBuilder("command")
* .redirectOutput(outputStream)
* .redirectError(errorStream)
* .start();
* int exitCode = process.waitFor();
* String stdout = outputStream.toString(StandardCharsets.UTF_8.name());
* String stderr = errorStream.toString(StandardCharsets.UTF_8.name());
* }</pre>
*/
public class ShellProcessBuilder {
private JadbDevice device;
private String command;
private ProcessBuilder.Redirect outRedirect = ProcessBuilder.Redirect.PIPE;
private OutputStream outOs = null;
private ProcessBuilder.Redirect errRedirect = ProcessBuilder.Redirect.PIPE;
private OutputStream errOs = null;
private boolean redirectErrorStream;
private Executor executor = null;
ShellProcessBuilder(JadbDevice device, String command) {
this.device = device;
this.command = command;
}
private void checkValidForWrite(ProcessBuilder.Redirect destination) {
if (destination.type() == ProcessBuilder.Redirect.Type.READ) {
throw new IllegalArgumentException("Redirect invalid for writing: " + destination);
}
}
/**
* Redirect stdout to the given destination. If set to anything other than
* {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getInputStream()} does nothing.
*
* @param destination where to redirect
* @return this
*/
public ShellProcessBuilder redirectOutput(ProcessBuilder.Redirect destination) {
checkValidForWrite(destination);
outRedirect = destination;
outOs = null;
return this;
}
/**
* Redirect stdout directly to the given output stream.
* <p>Note: this output steam will be called from a separate thread.</p>
*
* @param destination OutputStream to write
* @return this
*/
public ShellProcessBuilder redirectOutput(OutputStream destination) {
outRedirect = null;
outOs = destination;
return this;
}
/**
* Redirect stderr to the given destination. If set to anything other than
* {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getErrorStream()} does nothing.
*
* @param destination where to redirect
* @return this
*/
public ShellProcessBuilder redirectError(ProcessBuilder.Redirect destination) {
checkValidForWrite(destination);
errRedirect = destination;
errOs = null;
return this;
}
/**
* Redirect stderr directly to the given output stream.
* <p>Note: this output steam will be called from a separate thread.</p>
*
* @param destination OutputStream to write
* @return this
*/
public ShellProcessBuilder redirectError(OutputStream destination) {
errRedirect = null;
errOs = destination;
return this;
}
/**
* Set redirecting of the error stream directly to the output stream. If set, any {@code redirectError} calls are
* ignored, and the returned Process
*
* @param redirectErrorStream true to enable redirecting of the error stream
* @return this
*/
public ShellProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
this.redirectErrorStream = redirectErrorStream;
return this;
}
/**
* Set the {@link Executor} to use to run the process handling thread. If not set, uses
* {@link Executors#newSingleThreadExecutor()}.
*
* @param executor An executor
* @return this
*/
public ShellProcessBuilder useExecutor(Executor executor) {
this.executor = executor;
return this;
}
/**
* Starts the shell command.
*
* @return a {@link Process}
* @throws IOException
* @throws JadbException
*/
public ShellProcess start() throws IOException, JadbException {
Transport transport = null;
try {
final OutputStream outOs = getOutputStream(this.outOs, this.outRedirect, System.out);
InputStream outIs = getConnectedPipe(outOs);
final OutputStream errOs;
InputStream errIs;
if (redirectErrorStream) {
errOs = outOs;
errIs = NullInputStream.INSTANCE;
} else {
errOs = getOutputStream(this.errOs, this.errRedirect, System.err);
errIs = getConnectedPipe(errOs);
}
transport = device.getTransport();
final ShellProtocolTransport shellProtocolTransport = transport.startShellProtocol(this.command);
OutputStream inOs = shellProtocolTransport.getOutputStream();
FutureTask<Integer> transportTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
try (ShellProtocolTransport unused1 = shellProtocolTransport; OutputStream unused2 = outOs; OutputStream unused3 = errOs) {
return shellProtocolTransport.demuxOutput(outOs, errOs);
}
}
});
if (executor == null) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(transportTask);
service.shutdown();
} else {
executor.execute(transportTask);
}
return new ShellProcess(inOs, outIs, errIs, transportTask, shellProtocolTransport);
} catch (IOException | JadbException | RuntimeException e) {
if (transport != null) {
transport.close();
}
throw e;
}
}
private OutputStream getOutputStream(OutputStream os, ProcessBuilder.Redirect destination, OutputStream inherit) throws IOException {
if (os != null) {
return os;
}
switch (destination.type()) {
case PIPE:
return new PipedOutputStream();
case INHERIT:
return inherit;
case READ:
throw new IllegalArgumentException("Redirect invalid for writing: " + destination);
case WRITE:
return Files.newOutputStream(destination.file().toPath());
case APPEND:
return Files.newOutputStream(destination.file().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE);
default:
throw new IllegalArgumentException("Unknown redirect type: " + destination);
}
}
private InputStream getConnectedPipe(OutputStream os) throws IOException {
if (os instanceof PipedOutputStream) {
return new PipedInputStream((PipedOutputStream) os);
}
return NullInputStream.INSTANCE;
}
static class NullInputStream extends InputStream {
static final NullInputStream INSTANCE = new NullInputStream();
private NullInputStream() {
}
public int read() {
return -1;
}
public int available() {
return 0;
}
}
}

View File

@ -0,0 +1,154 @@
package se.vidstige.jadb;
import java.io.*;
class ShellProtocolTransport implements Closeable {
private final DataOutputStream output;
private final DataInputStream input;
ShellProtocolTransport(DataOutputStream outputStream, DataInputStream inputStream) {
output = outputStream;
input = inputStream;
}
// replace with Integer.toUnsignedLong in Java 8
private static long integerToUnsignedLong(int i) {
return ((long) (int) i) & 0xffffffffL;
}
// replace with Byte.toUnsignedInt in Java 8
private static int byteToUnsignedInt(byte b) {
return ((int) b) & 0xff;
}
private ShellMessageType readMessageType() throws IOException {
return ShellMessageType.fromId(input.readByte());
}
private long readDataLength() throws IOException {
return integerToUnsignedLong(Integer.reverseBytes(input.readInt()));
}
private void readDataTo(OutputStream out, long dataLength, byte[] buffer) throws IOException {
long remaining = dataLength;
while (remaining > 0) {
int len = (int) Math.min(remaining, buffer.length);
input.readFully(buffer, 0, len);
out.write(buffer, 0, len);
remaining -= len;
}
out.flush();
}
int demuxOutput(OutputStream outputStream, OutputStream errorStream) throws JadbException, IOException {
int exitCode = 0;
byte[] buf = new byte[256 * 1024];
try {
while (true) {
ShellMessageType messageType = readMessageType();
long length = readDataLength();
switch (messageType) {
case STDOUT:
readDataTo(outputStream, length, buf);
break;
case STDERR:
readDataTo(errorStream, length, buf);
break;
case EXIT:
if (length != 1) {
throw new JadbException("Expected only one byte for exitCode");
}
exitCode = byteToUnsignedInt(input.readByte());
break;
default:
// ignore;
break;
}
}
} catch (EOFException e) {
// ignore
}
return exitCode;
}
private void writeData(ShellMessageType type, byte[] buf, int off, int len) throws IOException {
output.writeByte(byteToUnsignedInt(type.getId()));
output.writeInt(Integer.reverseBytes(len));
output.write(buf, off, len);
}
OutputStream getOutputStream() {
return new ShellProtocolOutputStream(this);
}
@Override
public void close() throws IOException {
output.close();
input.close();
}
enum ShellMessageType {
STDIN((byte) 0), STDOUT((byte) 1), STDERR((byte) 2), EXIT((byte) 3), CLOSE_STDIN((byte) 4), WINDOW_SIZE_CHANGE((byte) 5), UNKNOWN(Byte.MIN_VALUE);
private final byte id;
ShellMessageType(byte id) {
this.id = id;
}
public static ShellMessageType fromId(byte b) {
switch (b) {
case 0:
return STDIN;
case 1:
return STDOUT;
case 2:
return STDERR;
case 3:
return EXIT;
case 4:
return CLOSE_STDIN;
case 5:
// unused
return WINDOW_SIZE_CHANGE;
default:
return UNKNOWN;
}
}
public byte getId() {
return id;
}
}
private static class ShellProtocolOutputStream extends OutputStream {
private final ShellProtocolTransport transport;
ShellProtocolOutputStream(ShellProtocolTransport transport) {
this.transport = transport;
}
@Override
public void write(int b) throws IOException {
write(new byte[]{(byte) b});
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
transport.writeData(ShellMessageType.STDIN, b, off, len);
}
@Override
public void flush() throws IOException {
transport.output.flush();
}
@Override
public void close() throws IOException {
transport.writeData(ShellMessageType.CLOSE_STDIN, new byte[0], 0, 0);
}
}
}

View File

@ -67,6 +67,12 @@ class Transport implements Closeable {
return new SyncTransport(dataOutput, dataInput);
}
public ShellProtocolTransport startShellProtocol(String command) throws IOException, JadbException {
send("shell,v2,raw:" + command);
verifyResponse();
return new ShellProtocolTransport(dataOutput, dataInput);
}
@Override
public void close() throws IOException {
dataInput.close();

View File

@ -63,7 +63,7 @@ class AdbProtocolHandler implements Runnable {
hostTransport(output, command);
} else if ("sync:".equals(command)) {
sync(output, input);
} else if (command.startsWith("shell:")) {
} else if (command.startsWith("shell")) {
shell(input, output, command);
return false;
} else if ("host:get-state".equals(command)) {
@ -118,7 +118,7 @@ class AdbProtocolHandler implements Runnable {
}
private void shell(DataInput input, DataOutputStream output, String command) throws IOException {
String shellCommand = command.substring("shell:".length());
String shellCommand = command.split(":", 2)[1];
output.writeBytes("OKAY");
shell(shellCommand, output, input);
}

View File

@ -1,31 +1,26 @@
package se.vidstige.jadb.test.integration;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import se.vidstige.jadb.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
public class RealDeviceTestCases {
private JadbConnection jadb;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(); //Must be public
private JadbConnection jadb;
@BeforeClass
public static void tryToStartAdbServer() {
@ -99,13 +94,74 @@ public class RealDeviceTestCases {
}
@SuppressWarnings("deprecation")
@Test
@Test
public void testShellExecuteTwice() throws Exception {
JadbDevice any = jadb.getAnyDevice();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
any.executeShell(bout, "ls /");
any.executeShell(bout, "ls", "-la", "/");
System.out.write(bout.toByteArray());
byte[] buf = bout.toByteArray();
System.out.write(buf, 0, buf.length);
}
@Test
public void testShellProcessBuilderStart() throws Exception {
JadbDevice any = jadb.getAnyDevice();
Process process = any.shellProcessBuilder("ls /").start();
AtomicReference<String> stdout = new AtomicReference<>();
AtomicReference<String> stderr = new AtomicReference<>();
Thread thread1 = gobbler(process.getInputStream(), stdout);
Thread thread2 = gobbler(process.getErrorStream(), stderr);
thread1.start();
thread2.start();
process.waitFor();
thread1.join();
thread2.join();
System.out.println(stdout.get());
System.out.println(stderr.get());
}
private Thread gobbler(final InputStream stream, final AtomicReference<String> out) {
return new Thread(new Runnable() {
@Override
public void run() {
out.set(new Scanner(stream).useDelimiter("\\A").next());
}
});
}
@Test
public void testShellExecuteProcessRedirectToOutputStream() throws Exception {
JadbDevice any = jadb.getAnyDevice();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
Process process = any.shellProcessBuilder("ls /")
.redirectOutput(out)
.redirectError(err)
.start();
process.waitFor();
System.out.println(out.toString(StandardCharsets.UTF_8.name()));
System.out.println(err.toString(StandardCharsets.UTF_8.name()));
}
@Test
public void testShellExecuteProcessRedirectErrorStream() throws Exception {
JadbDevice any = jadb.getAnyDevice();
Process process = any.shellProcessBuilder("ls /").redirectErrorStream(true).start();
String stdout = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
process.waitFor();
System.out.println(stdout);
}
@Test
public void testShellExecuteProcessDestroy() throws Exception {
JadbDevice anyDevice = jadb.getAnyDevice();
ExecutorService executor = Executors.newSingleThreadExecutor();
ShellProcess process = anyDevice.shellProcessBuilder("sleep 30").redirectErrorStream(true).useExecutor(executor).start();
process.destroy();
assertEquals(process.waitFor(), 9);
executor.shutdown();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
@Test
@ -135,11 +191,10 @@ public class RealDeviceTestCases {
}
/**
* @see #testConnectionToTcpDevice()
*
* @throws IOException
* @throws JadbException
* @throws ConnectionToRemoteDeviceException
* @see #testConnectionToTcpDevice()
*/
@Test
public void testDisconnectionToTcpDevice() throws IOException, JadbException, ConnectionToRemoteDeviceException {

View File

@ -12,18 +12,55 @@ import se.vidstige.jadb.test.fakes.FakeAdbServer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Scanner;
public class MockedTestCases {
private FakeAdbServer server;
private JadbConnection connection;
private static long parseDate(String date) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return dateFormat.parse(date).getTime();
}
private static void assertHasFile(String expPath, int expSize, long expModifyTime, List<RemoteFile> actualFiles) {
for (RemoteFile file : actualFiles) {
if (expPath.equals(file.getPath())) {
if (file.isDirectory()) {
Assert.fail("File " + expPath + " was listed as a dir!");
} else if (expSize != file.getSize() || expModifyTime != file.getLastModified()) {
Assert.fail("File " + expPath + " exists but has incorrect properties!");
} else {
return;
}
}
}
Assert.fail("File " + expPath + " could not be found!");
}
private static void assertHasDir(String expPath, long expModifyTime, List<RemoteFile> actualFiles) {
for (RemoteFile file : actualFiles) {
if (expPath.equals(file.getPath())) {
if (!file.isDirectory()) {
Assert.fail("Dir " + expPath + " was listed as a file!");
} else if (expModifyTime != file.getLastModified()) {
Assert.fail("Dir " + expPath + " exists but has incorrect properties!");
} else {
return;
}
}
}
Assert.fail("Dir " + expPath + " could not be found!");
}
@Before
public void setUp() throws Exception {
server = new FakeAdbServer(15037);
@ -130,6 +167,39 @@ public class MockedTestCases {
device.executeShell("echo", "h¡t]&poli");
}
@Test
public void testExecuteShellProtocol() throws Exception {
server.add("serial-123");
server.expectShell("serial-123", "ls -l").returns(buildStream(null, null, 0));
server.expectShell("serial-123", "ls foobar").returns(buildStream("123", "456", 0));
JadbDevice device = connection.getDevices().get(0);
Assert.assertEquals(device.shellProcessBuilder("ls", "-l").start().waitFor(), 0);
Process process = device.shellProcessBuilder("ls", "foobar").redirectErrorStream(true).start();
Assert.assertEquals(new Scanner(process.getInputStream()).useDelimiter("\\A").next(), "123456");
Assert.assertEquals(process.waitFor(), 0);
}
private String buildStream(String out, String err, int exitCode) throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(os);
if (out != null) {
os.write(1);
dos.writeInt(Integer.reverseBytes(out.length()));
os.write(out.getBytes(StandardCharsets.US_ASCII));
}
if (err != null) {
os.write(2);
dos.writeInt(Integer.reverseBytes(err.length()));
os.write(err.getBytes(StandardCharsets.US_ASCII));
}
os.write(3); // exitcode stream
dos.writeInt(Integer.reverseBytes(1));
os.write(exitCode);
return os.toString(StandardCharsets.US_ASCII.name());
}
@Test
public void testFileList() throws Exception {
server.add("serial-123");
@ -150,39 +220,4 @@ public class MockedTestCases {
assertHasFile("effective java vol. 7.epub", 0xCAFE, 0xBABE, files);
assertHasFile("\uB9AC\uADF8 \uC624\uBE0C \uB808\uC804\uB4DC", 240, 9001, files);
}
private static long parseDate(String date) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return dateFormat.parse(date).getTime();
}
private static void assertHasFile(String expPath, int expSize, long expModifyTime, List<RemoteFile> actualFiles) {
for (RemoteFile file : actualFiles) {
if (expPath.equals(file.getPath())) {
if (file.isDirectory()) {
Assert.fail("File " + expPath + " was listed as a dir!");
} else if (expSize != file.getSize() || expModifyTime != file.getLastModified()) {
Assert.fail("File " + expPath + " exists but has incorrect properties!");
} else {
return;
}
}
}
Assert.fail("File " + expPath + " could not be found!");
}
private static void assertHasDir(String expPath, long expModifyTime, List<RemoteFile> actualFiles) {
for (RemoteFile file : actualFiles) {
if (expPath.equals(file.getPath())) {
if (!file.isDirectory()) {
Assert.fail("Dir " + expPath + " was listed as a file!");
} else if (expModifyTime != file.getLastModified()) {
Assert.fail("Dir " + expPath + " exists but has incorrect properties!");
} else {
return;
}
}
}
Assert.fail("Dir " + expPath + " could not be found!");
}
}