Evacuate unpacking logic from RN

Reviewed By: amnn

Differential Revision: D4186902

fbshipit-source-id: 02d8fe176940a678dc8f8d9c0bcf43732f45bde5
This commit is contained in:
Michał Gregorczyk 2016-11-17 03:01:11 -08:00 committed by Facebook Github Bot
parent 112bdc99dc
commit 32670371e4
8 changed files with 35 additions and 1001 deletions

View File

@ -35,7 +35,4 @@ public class ReactMarkerConstants {
"CREATE_UI_MANAGER_MODULE_CONSTANTS_END";
public static final String CREATE_MODULE_START = "CREATE_MODULE_START";
public static final String CREATE_MODULE_END = "CREATE_MODULE_END";
public static final String UNPACKER_CHECK_START = "UNPACKER_CHECK_START";
public static final String UNPACKER_CHECK_END = "UNPACKER_CHECK_END";
public static final String UNPACKER_BUNDLE_EXTRACTED = "UNPACKER_BUNDLE_EXTRACTED";
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
/**
* Bundle loader using optimized bundle API
*/
public class OptimizedJSBundleLoader extends JSBundleLoader {
private String mPath;
private String mSourceURL;
private int mLoadFlags;
public OptimizedJSBundleLoader(String path, String sourceURL, int loadFlags) {
mLoadFlags = loadFlags;
mSourceURL = sourceURL;
mPath = path;
}
@Override
public void loadScript(CatalystInstanceImpl instance) {
instance.loadScriptFromOptimizedBundle(mPath, mSourceURL, mLoadFlags);
}
@Override
public String getSourceUrl() {
return mSourceURL;
}
}

View File

@ -1,484 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
import android.content.Context;
import android.content.res.AssetManager;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.soloader.FileLocker;
import com.facebook.soloader.SysUtil;
import com.facebook.systrace.Systrace;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Semaphore;
import javax.annotation.Nullable;
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;
/**
* JSBundleLoader capable of unpacking specified files necessary for executing
* JS bundle stored in optimized format.
*/
public class UnpackingJSBundleLoader extends JSBundleLoader {
/**
* Name of the lock files. Multiple processes can be spawned off the same app
* and we need to guarantee that at most one unpacks files at any time. To
* make that work any process is required to hold file system lock on
* LOCK_FILE when checking whether files should be unpacked and unpacking
* them.
*/
static final String LOCK_FILE = "unpacking-bundle-loader.lock";
/**
* Existence of this file indicates that the last unpacking operation finished
* before the app was killed or crashed. File with this name is created in the
* destination directory as the last one. If it is present it means that
* all the files that needed to be fsynced were fsynced and their content is
* what it should be.
*/
static final String DOT_UNPACKED_FILE = ".unpacked";
private static final int IO_BUFFER_SIZE = 16 * 1024;
/**
* Where all the files should go to.
*/
private final File mDirectoryPath;
private final String mSourceURL;
private final Context mContext;
private final int mLoadFlags;
private final boolean mFinishOnBackgroundThread;
private final @Nullable Runnable mOnUnpackedCallback;
/**
* True if prepare was called.
*/
private boolean mPrepared;
/**
* Synchronizes unpacking within this process.
*/
private static final Semaphore sProcessLock = new Semaphore(1);
/**
* Synchronizes unpacking across multiple processes.
*/
private @Nullable FileLocker mFileLocker;
/**
* Description of what needs to be unpacked.
*/
private final Unpacker[] mUnpackers;
/* package */ UnpackingJSBundleLoader(Builder builder) {
mContext = Assertions.assertNotNull(builder.context);
mDirectoryPath = Assertions.assertNotNull(builder.destinationPath);
mSourceURL = Assertions.assertNotNull(builder.sourceURL);
mUnpackers = builder.unpackers.toArray(new Unpacker[builder.unpackers.size()]);
mLoadFlags = builder.loadFlags;
mFinishOnBackgroundThread = builder.finishOnBackgroundThread;
mOnUnpackedCallback = builder.callback;
mFileLocker = null;
mPrepared = false;
}
/**
* Checks if any file needs to be extracted again, and if so, clears the destination
* directory and unpacks everything again.
*
* This method does not do anything if called for the second time
*/
public synchronized void prepare() {
if (mPrepared) {
return;
}
ReactMarker.logMarker(ReactMarkerConstants.UNPACKER_CHECK_START);
boolean unpacked = false;
try {
lock();
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "UnpackingJSBundleLoader.prepare");
try {
unpacked = prepareLocked();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
if (!mFinishOnBackgroundThread || !unpacked) {
unlock();
}
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
if (unpacked) {
ReactMarker.logMarker(ReactMarkerConstants.UNPACKER_BUNDLE_EXTRACTED);
}
if (unpacked && mOnUnpackedCallback != null) {
mOnUnpackedCallback.run();
}
ReactMarker.logMarker(ReactMarkerConstants.UNPACKER_CHECK_END);
mPrepared = true;
}
private boolean prepareLocked() throws IOException {
final File dotFinishedFilePath = new File(mDirectoryPath, DOT_UNPACKED_FILE);
boolean shouldReconstruct = !mDirectoryPath.exists() || !dotFinishedFilePath.exists();
final byte[] buffer = new byte[IO_BUFFER_SIZE];
for (int i = 0; i < mUnpackers.length && !shouldReconstruct; ++i) {
shouldReconstruct = mUnpackers[i].shouldReconstructDir(mContext, buffer);
}
if (!shouldReconstruct) {
return false;
}
boolean succeeded = false;
try {
SysUtil.dumbDeleteRecursive(mDirectoryPath);
if (!mDirectoryPath.mkdirs()) {
throw new IOException("Coult not create the destination directory");
}
for (Unpacker unpacker : mUnpackers) {
unpacker.unpack(mContext, buffer);
}
if (mFinishOnBackgroundThread) {
finishUnpackingOnBackgroundThread();
} else {
finishUnpacking();
}
succeeded = true;
} finally {
// In case of failure do yourself a favor and remove partially initialized state.
if (!succeeded) {
SysUtil.dumbDeleteRecursive(mDirectoryPath);
}
}
return true;
}
private void finishUnpacking() throws IOException {
for (Unpacker unpacker : mUnpackers) {
unpacker.finishUnpacking(mContext);
}
final File dotFinishedFilePath = new File(mDirectoryPath, DOT_UNPACKED_FILE);
if (!dotFinishedFilePath.createNewFile()) {
throw new IOException("Could not create .unpacked file");
}
// It would be nice to fsync a few directories and files here. The thing is, if we crash and
// lose some data then it should be noticed on the next prepare invocation and the directory
// will be reconstructed. It is only crucial to fsync those files whose content is not
// verified on each start. Everything else is a tradeoff between perf with no crashes
// situation and perf when user experiences crashes. Fortunately Unpackers corresponding
// to files whose content is not checked handle fsyncs themselves.
}
/**
* Finishes unpacking and unlocks the unpacker on a background thread.
*/
private void finishUnpackingOnBackgroundThread() {
new Thread(new Runnable() {
@Override
public void run() {
Systrace.beginSection(
TRACE_TAG_REACT_JAVA_BRIDGE,
"UnpackingJSBundleLoader.finishUnpackingOnBackgroundThread()");
try {
finishUnpacking();
unlock();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
}).start();
}
@Override
public void loadScript(CatalystInstanceImpl instance) {
prepare();
instance.loadScriptFromOptimizedBundle(
mDirectoryPath.getPath(),
mSourceURL,
mLoadFlags);
}
private void lock() throws IOException, InterruptedException {
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "UnpackingJSBundleLoader.lock");
try {
sProcessLock.acquire();
boolean success = false;
try {
Assertions.assertCondition(mFileLocker == null);
mFileLocker = FileLocker.lock(new File(mContext.getFilesDir(), LOCK_FILE));
success = true;
} finally {
if (!success) {
sProcessLock.release();
}
}
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
private void unlock() throws IOException {
Assertions.assertNotNull(mFileLocker).close();
mFileLocker = null;
sProcessLock.release();
}
@Override
public String getSourceUrl() {
return mSourceURL;
}
static void fsync(File path) throws IOException {
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "UnpackingJSBundleLoader.fsync");
try (RandomAccessFile file = new RandomAccessFile(path, "r")) {
file.getFD().sync();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
/**
* Reads all the bytes (but no more that maxSize) from given input stream through ioBuffer
* and returns byte array containing all the read bytes.
*/
static byte[] readBytes(InputStream is, byte[] ioBuffer, int maxSize) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
copyBytes(baos, is, ioBuffer, maxSize);
return baos.toByteArray();
}
/**
* Pumps all the bytes (but no more that maxSize) from given input stream through ioBuffer
* to given output stream and returns number of moved bytes.
*/
static int copyBytes(
OutputStream os,
InputStream is,
byte[] ioBuffer,
int maxSize) throws IOException {
int totalSize = 0;
while (totalSize < maxSize) {
int rc = is.read(ioBuffer, 0, Math.min(maxSize - totalSize, ioBuffer.length));
if (rc == -1) {
break;
}
os.write(ioBuffer, 0, rc);
totalSize += rc;
}
return totalSize;
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private @Nullable Context context;
private @Nullable File destinationPath;
private @Nullable String sourceURL;
private final ArrayList<Unpacker> unpackers;
private int loadFlags;
private boolean finishOnBackgroundThread;
private @Nullable Runnable callback;
public Builder() {
this.unpackers = new ArrayList<Unpacker>();
context = null;
destinationPath = null;
sourceURL = null;
loadFlags = 0;
finishOnBackgroundThread = true;
callback = null;
}
public Builder setContext(Context context) {
this.context = context;
return this;
}
public Builder setDestinationPath(File destinationPath) {
this.destinationPath = destinationPath;
return this;
}
public Builder setSourceURL(String sourceURL) {
this.sourceURL = sourceURL;
return this;
}
public Builder setLoadFlags(int loadFlags) {
this.loadFlags = loadFlags;
return this;
}
public Builder setFinishOnBackgroundThread(boolean finishOnBackgroundThread) {
this.finishOnBackgroundThread = finishOnBackgroundThread;
return this;
}
/**
* Adds a file for unpacking. Content of extracted file is not checked on each
* start against content of the file bundled in apk.
*/
public Builder unpackFile(String nameInApk, String destFileName) {
unpackers.add(new ExistenceCheckingUnpacker(nameInApk, destFileName));
return this;
}
/**
* Adds a file for unpacking. Content of extracted file is compared on each
* start with content of the same file bundled in apk. It is usefull for
* detecting bundle/app changes.
*/
public Builder checkAndUnpackFile(String nameInApk, String destFileName) {
unpackers.add(new ContentCheckingUnpacker(nameInApk, destFileName));
return this;
}
/**
* Adds arbitrary unpacker. Usefull for injecting mocks.
*/
Builder addUnpacker(Unpacker u) {
unpackers.add(u);
return this;
}
public Builder setOnUnpackedCallback(Runnable callback) {
this.callback = callback;
return this;
}
public UnpackingJSBundleLoader build() {
Assertions.assertNotNull(destinationPath);
for (int i = 0; i < unpackers.size(); ++i) {
unpackers.get(i).setDestinationDirectory(destinationPath);
}
return new UnpackingJSBundleLoader(this);
}
}
/**
* Abstraction for dealing with unpacking single file from apk.
*/
static abstract class Unpacker {
protected final String mNameInApk;
private final String mFileName;
protected @Nullable File mDestinationFilePath;
public Unpacker(String nameInApk, String fileName) {
mNameInApk = nameInApk;
mFileName = fileName;
}
public void setDestinationDirectory(File destinationDirectoryPath) {
mDestinationFilePath = new File(destinationDirectoryPath, mFileName);
}
public abstract boolean shouldReconstructDir(Context context, byte[] ioBuffer)
throws IOException;
public void unpack(Context context, byte[] ioBuffer) throws IOException {
AssetManager am = context.getAssets();
try (InputStream is = am.open(mNameInApk, AssetManager.ACCESS_STREAMING)) {
try (FileOutputStream fileOutputStream = new FileOutputStream(
Assertions.assertNotNull(mDestinationFilePath))) {
copyBytes(fileOutputStream, is, ioBuffer, Integer.MAX_VALUE);
}
}
}
public void finishUnpacking(Context context) throws IOException {
}
}
/**
* Deals with unpacking files whose content is not checked on each start and
* need to be fsynced after unpacking.
*/
static class ExistenceCheckingUnpacker extends Unpacker {
public ExistenceCheckingUnpacker(String nameInApk, String fileName) {
super(nameInApk, fileName);
}
@Override
public boolean shouldReconstructDir(Context context, byte[] ioBuffer) {
return !Assertions.assertNotNull(mDestinationFilePath).exists();
}
@Override
public void finishUnpacking(Context context) throws IOException {
fsync(Assertions.assertNotNull(mDestinationFilePath));
}
}
/**
* Deals with unpacking files whose content is checked on each start and thus
* do not require fsync.
*/
static class ContentCheckingUnpacker extends Unpacker {
public ContentCheckingUnpacker(String nameInApk, String fileName) {
super(nameInApk, fileName);
}
@Override
public boolean shouldReconstructDir(Context context, byte[] ioBuffer) throws IOException {
if (!Assertions.assertNotNull(mDestinationFilePath).exists()) {
return true;
}
AssetManager am = context.getAssets();
final byte[] assetContent;
try (InputStream assetStream = am.open(mNameInApk, AssetManager.ACCESS_STREAMING)) {
assetContent = readBytes(assetStream, ioBuffer, Integer.MAX_VALUE);
}
final byte[] fileContent;
try (InputStream fileStream = new FileInputStream(
Assertions.assertNotNull(mDestinationFilePath))) {
fileContent = readBytes(fileStream, ioBuffer, assetContent.length + 1);
}
return !Arrays.equals(assetContent, fileContent);
}
}
}

View File

@ -1,41 +0,0 @@
include_defs('//ReactAndroid/DEFS')
STANDARD_TEST_SRCS = [
'*Test.java',
]
android_library(
name = 'testhelpers',
srcs = glob(['*.java'], excludes = STANDARD_TEST_SRCS),
deps = [
react_native_dep('third-party/java/junit:junit'),
react_native_dep('third-party/java/mockito:mockito'),
],
visibility = [
'PUBLIC'
],
)
robolectric3_test(
name = 'bridge',
# Please change the contact to the oncall of your team
contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'],
srcs = glob(STANDARD_TEST_SRCS),
deps = [
':testhelpers',
react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'),
react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'),
react_native_dep('third-party/java/junit:junit'),
react_native_dep('third-party/java/mockito:mockito'),
react_native_dep('third-party/java/robolectric3/robolectric:robolectric'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/cxxbridge:bridge'),
],
visibility = [
'PUBLIC'
],
)
project_config(
test_target = ':bridge',
)

View File

@ -1,85 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
@PrepareForTest({UnpackingJSBundleLoader.class})
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@RunWith(RobolectricTestRunner.class)
public class ContentCheckingUnpackerTest extends UnpackerTestBase {
@Rule
public PowerMockRule rule = new PowerMockRule();
private UnpackingJSBundleLoader.ContentCheckingUnpacker mUnpacker;
@Before
public void setUp() throws IOException {
super.setUp();
mUnpacker = new UnpackingJSBundleLoader.ContentCheckingUnpacker(
NAME_IN_APK,
DESTINATION_NAME);
mUnpacker.setDestinationDirectory(folder.getRoot());
}
@Test
public void testReconstructsIfFileDoesNotExist() throws IOException {
assertTrue(mUnpacker.shouldReconstructDir(mContext, mIOBuffer));
}
@Test
public void testReconstructsIfContentDoesNotMatch() throws IOException {
try (FileOutputStream fos = new FileOutputStream(mDestinationPath)) {
fos.write(ASSET_DATA, 0, ASSET_DATA.length - 1);
fos.write((byte) (ASSET_DATA[ASSET_DATA.length - 1] + 1));
}
assertTrue(mUnpacker.shouldReconstructDir(mContext, mIOBuffer));
}
@Test
public void testDoesNotReconstructIfContentMatches() throws IOException {
try (FileOutputStream fos = new FileOutputStream(mDestinationPath)) {
fos.write(ASSET_DATA);
}
assertFalse(mUnpacker.shouldReconstructDir(mContext, mIOBuffer));
}
@Test
public void testUnpacksFile() throws IOException {
mUnpacker.unpack(mContext, mIOBuffer);
assertTrue(mDestinationPath.exists());
try (InputStream is = new FileInputStream(mDestinationPath)) {
byte[] storedData = UnpackingJSBundleLoader.readBytes(is, mIOBuffer, Integer.MAX_VALUE);
assertArrayEquals(ASSET_DATA, storedData);
}
}
}

View File

@ -1,78 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.times;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
@PrepareForTest({UnpackingJSBundleLoader.class})
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@RunWith(RobolectricTestRunner.class)
public class ExistenceCheckingUnpackerTest extends UnpackerTestBase {
@Rule
public PowerMockRule rule = new PowerMockRule();
private UnpackingJSBundleLoader.ExistenceCheckingUnpacker mUnpacker;
@Before
public void setUp() throws IOException {
super.setUp();
mUnpacker = new UnpackingJSBundleLoader.ExistenceCheckingUnpacker(
NAME_IN_APK,
DESTINATION_NAME);
mUnpacker.setDestinationDirectory(folder.getRoot());
}
@Test
public void testReconstructsIfFileDoesNotExist() {
assertTrue(mUnpacker.shouldReconstructDir(mContext, mIOBuffer));
}
@Test
public void testDoesNotReconstructIfFileExists() throws IOException {
mDestinationPath.createNewFile();
assertFalse(mUnpacker.shouldReconstructDir(mContext, mIOBuffer));
}
@Test
public void testUnpacksFile() throws IOException {
mUnpacker.unpack(mContext, mIOBuffer);
assertTrue(mDestinationPath.exists());
try (InputStream is = new FileInputStream(mDestinationPath)) {
byte[] storedData = UnpackingJSBundleLoader.readBytes(is, mIOBuffer, Integer.MAX_VALUE);
assertArrayEquals(ASSET_DATA, storedData);
}
}
@Test
public void testFsyncsAfterUnpacking() throws IOException {
mockStatic(UnpackingJSBundleLoader.class);
mUnpacker.finishUnpacking(mContext);
verifyStatic(times(1));
UnpackingJSBundleLoader.fsync(mDestinationPath);
}
}

View File

@ -1,95 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
import android.content.Context;
import android.content.res.AssetManager;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class UnpackerTestBase {
static final String NAME_IN_APK = "nameInApk";
static final String DESTINATION_NAME = "destination";
static final byte[] ASSET_DATA = new byte[]{(byte) 1, (byte) 101, (byte) 50};
@Rule
public TemporaryFolder folder = new TemporaryFolder();
File mDestinationPath;
byte[] mIOBuffer;
Context mContext;
AssetManager mAssetManager;
public void setUp() throws IOException {
mDestinationPath = new File(folder.getRoot(), DESTINATION_NAME);
mIOBuffer = new byte[16 * 1024];
mContext = mock(Context.class);
mAssetManager = mock(AssetManager.class);
when(mContext.getAssets()).thenReturn(mAssetManager);
when(mAssetManager.open(eq(NAME_IN_APK), anyInt()))
.then(new Answer<FileInputStream>() {
@Override
public FileInputStream answer(InvocationOnMock invocation) throws Throwable {
final ByteArrayInputStream bais = new ByteArrayInputStream(ASSET_DATA);
final FileInputStream fis = mock(FileInputStream.class);
when(fis.read())
.then(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return bais.read();
}
});
when(fis.read(any(byte[].class)))
.then(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return bais.read((byte[]) invocation.getArguments()[0]);
}
});
when(fis.read(any(byte[].class), any(int.class), any(int.class)))
.then(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return bais.read(
(byte[]) invocation.getArguments()[0],
(int) invocation.getArguments()[1],
(int) invocation.getArguments()[2]);
}
});
when(fis.available()).then(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return bais.available();
}
});
return fis;
}
});
}
}

View File

@ -1,215 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.cxxbridge;
import android.content.Context;
import com.facebook.soloader.SoLoader;
import java.io.File;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@RunWith(RobolectricTestRunner.class)
public class UnpackingJSBundleLoaderTest {
static {
SoLoader.setInTestMode();
}
private static final String URL = "http://this.is.an.url";
private static final int MOCK_UNPACKERS_NUM = 2;
private static final int UNPACKER_TEST_FLAGS = 129;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private File mDestinationPath;
private File mFilesPath;
private UnpackingJSBundleLoader.Builder mBuilder;
private Context mContext;
private CatalystInstanceImpl mCatalystInstanceImpl;
private UnpackingJSBundleLoader.Unpacker[] mMockUnpackers;
private Runnable mCallback;
@Before
public void setUp() throws IOException {
mDestinationPath = folder.newFolder("destination");
mFilesPath = folder.newFolder("files");
mContext = mock(Context.class);
when(mContext.getFilesDir()).thenReturn(mFilesPath);
mCatalystInstanceImpl = mock(CatalystInstanceImpl.class);
mBuilder = UnpackingJSBundleLoader.newBuilder()
.setDestinationPath(mDestinationPath)
.setSourceURL(URL)
.setContext(mContext)
.setFinishOnBackgroundThread(false);
mMockUnpackers = new UnpackingJSBundleLoader.Unpacker[MOCK_UNPACKERS_NUM];
for (int i = 0; i < mMockUnpackers.length; ++i) {
mMockUnpackers[i] = mock(UnpackingJSBundleLoader.Unpacker.class);
}
mCallback = mock(Runnable.class);
}
private void addUnpackers() {
for (UnpackingJSBundleLoader.Unpacker unpacker : mMockUnpackers) {
mBuilder.addUnpacker(unpacker);
}
}
@Test
public void testGetSourceUrl() {
assertEquals(URL, mBuilder.build().getSourceUrl());
}
@Test
public void testCreatesDotUnpackedFile() throws IOException {
mBuilder.build().prepare();
assertTrue(new File(mDestinationPath, UnpackingJSBundleLoader.DOT_UNPACKED_FILE).exists());
}
@Test
public void testCreatesLockFile() throws IOException {
mBuilder.build().prepare();
assertTrue(new File(mFilesPath, UnpackingJSBundleLoader.LOCK_FILE).exists());
}
@Test
public void testCallsAppropriateInstanceMethod() throws IOException {
mBuilder.build().loadScript(mCatalystInstanceImpl);
verify(mCatalystInstanceImpl).loadScriptFromOptimizedBundle(
eq(mDestinationPath.getPath()),
eq(URL),
eq(0));
verifyNoMoreInteractions(mCatalystInstanceImpl);
}
@Test
public void testSetLoadFlags() throws IOException {
mBuilder.setLoadFlags(UNPACKER_TEST_FLAGS)
.build()
.loadScript(mCatalystInstanceImpl);
verify(mCatalystInstanceImpl).loadScriptFromOptimizedBundle(
eq(mDestinationPath.getPath()),
eq(URL),
eq(UNPACKER_TEST_FLAGS));
}
@Test
public void testLoadScriptUnpacks() {
mBuilder.build().loadScript(mCatalystInstanceImpl);
assertTrue(new File(mDestinationPath, UnpackingJSBundleLoader.DOT_UNPACKED_FILE).exists());
}
@Test
public void testPrepareCallDoesNotRecreateDirIfNotNecessary() throws IOException {
mBuilder.build().prepare();
addUnpackers();
mBuilder.build().prepare();
for (UnpackingJSBundleLoader.Unpacker unpacker : mMockUnpackers) {
verify(unpacker).setDestinationDirectory(mDestinationPath);
verify(unpacker).shouldReconstructDir(
same(mContext),
any(byte[].class));
verifyNoMoreInteractions(unpacker);
}
}
@Test
public void testShouldReconstructDirForcesRecreation() throws IOException {
mBuilder.build().prepare();
addUnpackers();
when(mMockUnpackers[0].shouldReconstructDir(
same(mContext),
any(byte[].class)))
.thenReturn(true);
mBuilder.build().prepare();
verify(mMockUnpackers[0]).shouldReconstructDir(
same(mContext),
any(byte[].class));
for (UnpackingJSBundleLoader.Unpacker unpacker : mMockUnpackers) {
verify(unpacker).setDestinationDirectory(mDestinationPath);
verify(unpacker).unpack(
same(mContext),
any(byte[].class));
verify(unpacker).finishUnpacking(same(mContext));
verifyNoMoreInteractions(unpacker);
}
}
@Test
public void testDirectoryReconstructionRemovesDir() throws IOException {
mBuilder.build().prepare();
final File aFile = new File(mDestinationPath, "a_file");
aFile.createNewFile();
when(mMockUnpackers[0].shouldReconstructDir(
same(mContext),
any(byte[].class)))
.thenReturn(true);
addUnpackers();
mBuilder.build().prepare();
assertFalse(aFile.exists());
}
@Test(expected = RuntimeException.class)
public void testDropsDirectoryOnException() throws IOException {
doThrow(new IOException("An expected IOException"))
.when(mMockUnpackers[0]).unpack(
same(mContext),
any(byte[].class));
try {
mBuilder.addUnpacker(mMockUnpackers[0]).build().prepare();
} finally {
assertFalse(mDestinationPath.exists());
}
}
@Test
public void testCallbackIsCalledAfterUnpack() {
mBuilder.setOnUnpackedCallback(mCallback).build().prepare();
verify(mCallback).run();
}
@Test
public void testCallbackIsNotCalledIfNothingIsUnpacked() {
mBuilder.build().prepare();
mBuilder.setOnUnpackedCallback(mCallback).build().prepare();
verifyNoMoreInteractions(mCallback);
}
}