mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-18 20:26:19 +08:00
Unpack files required by optimized bundle format
Reviewed By: tadeuzagallo Differential Revision: D3522855 fbshipit-source-id: 2d14db33ce9b98ea1aeea5a12e292e5926e43796
This commit is contained in:
committed by
Facebook Github Bot 2
parent
e632025917
commit
a665914d18
@@ -14,6 +14,8 @@ import android.content.Context;
|
||||
import com.facebook.react.devsupport.DebugServerException;
|
||||
import com.facebook.react.devsupport.DevServerHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct
|
||||
* bundle through {@link ReactBridge}.
|
||||
@@ -99,6 +101,19 @@ public abstract class JSBundleLoader {
|
||||
};
|
||||
}
|
||||
|
||||
public static JSBundleLoader createUnpackingBundleLoader(
|
||||
final Context context,
|
||||
final String sourceURL,
|
||||
final String bundleName) {
|
||||
return UnpackingJSBundleLoader.newBuilder()
|
||||
.setContext(context)
|
||||
.setSourceURL(sourceURL)
|
||||
.setDestinationPath(new File(context.getFilesDir(), "optimized-bundle"))
|
||||
.checkAndUnpackFile(bundleName + ".meta", "bundle.meta")
|
||||
.unpackFile(bundleName, "bundle.js")
|
||||
.build();
|
||||
}
|
||||
|
||||
public abstract void loadScript(CatalystInstanceImpl instance);
|
||||
public abstract String getSourceUrl();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 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.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 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;
|
||||
|
||||
/**
|
||||
* 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()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any file needs to be extracted again, and if so, clears the destination
|
||||
* directory and unpacks everything again.
|
||||
*/
|
||||
/* package */ void prepare() {
|
||||
final File lockFilePath = new File(mContext.getFilesDir(), LOCK_FILE);
|
||||
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "UnpackingJSBundleLoader.prepare");
|
||||
try (FileLocker lock = FileLocker.lock(lockFilePath)) {
|
||||
prepareLocked();
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
} finally {
|
||||
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareLocked() throws IOException {
|
||||
final File dotFinishedFilePath = new File(mDirectoryPath, DOT_UNPACKED_FILE);
|
||||
boolean shouldReconstruct = !mDirectoryPath.exists() || !dotFinishedFilePath.exists();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 (!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.
|
||||
|
||||
succeeded = true;
|
||||
} finally {
|
||||
// In case of failure do yourself a favor and remove partially initialized state.
|
||||
if (!succeeded) {
|
||||
SysUtil.dumbDeleteRecursive(mDirectoryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadScript(CatalystInstanceImpl instance) {
|
||||
prepare();
|
||||
// TODO(12128379): add instance method that would take bundle directory
|
||||
instance.loadScriptFromFile(
|
||||
new File(mDirectoryPath, "bundle.js").getPath(),
|
||||
mSourceURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSourceUrl() {
|
||||
return mSourceURL;
|
||||
}
|
||||
|
||||
static void fsync(File path) throws IOException {
|
||||
try (RandomAccessFile file = new RandomAccessFile(path, "r")) {
|
||||
file.getFD().sync();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public Builder() {
|
||||
this.unpackers = new ArrayList<Unpacker>();
|
||||
context = null;
|
||||
destinationPath = null;
|
||||
sourceURL = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 unpack(Context context, byte[] ioBuffer) throws IOException {
|
||||
super.unpack(context, ioBuffer);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
ReactAndroid/src/test/java/com/facebook/react/cxxbridge/BUCK
Normal file
40
ReactAndroid/src/test/java/com/facebook/react/cxxbridge/BUCK
Normal file
@@ -0,0 +1,40 @@
|
||||
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/cxxbridge:bridge'),
|
||||
],
|
||||
visibility = [
|
||||
'PUBLIC'
|
||||
],
|
||||
)
|
||||
|
||||
project_config(
|
||||
test_target = ':bridge',
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.unpack(mContext, mIOBuffer);
|
||||
verifyStatic(times(1));
|
||||
UnpackingJSBundleLoader.fsync(mDestinationPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@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;
|
||||
|
||||
@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);
|
||||
|
||||
mMockUnpackers = new UnpackingJSBundleLoader.Unpacker[MOCK_UNPACKERS_NUM];
|
||||
for (int i = 0; i < mMockUnpackers.length; ++i) {
|
||||
mMockUnpackers[i] = mock(UnpackingJSBundleLoader.Unpacker.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).loadScriptFromFile(
|
||||
eq(new File(mDestinationPath, "bundle.js").getPath()),
|
||||
eq(URL));
|
||||
verifyNoMoreInteractions(mCatalystInstanceImpl);
|
||||
}
|
||||
|
||||
@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));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user