Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ObjectArena and ArenaRef to solve JNI Global Reference Issue #1176

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions firestore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ set(android_SRCS
src/android/wrapper.h
src/android/write_batch_android.cc
src/android/write_batch_android.h
src/jni/arena_ref.cc
src/jni/arena_ref.h
src/jni/array.h
src/jni/array_list.cc
src/jni/array_list.h
Expand Down
1 change: 1 addition & 0 deletions firestore/integration_test_internal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ set(FIREBASE_INTEGRATION_TEST_ANDROID_TEST_SRCS
src/android/snapshot_metadata_android_test.cc
src/android/timestamp_android_test.cc
src/android/transaction_options_android_test.cc
src/jni/arena_ref_test.cc
src/jni/declaration_test.cc
src/jni/env_test.cc
src/jni/object_test.cc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ TEST_F(DocumentReferenceTest, RecoverFirestore) {
DocumentReference doc = Document();
ASSERT_EQ(db, doc.firestore()); // Sanity check

jni::Object doc_java = GetInternal(doc)->ToJava();
jni::Local<jni::Object> doc_java = GetInternal(doc)->ToJava();
result = DocumentReferenceInternal::Create(env, doc_java);
ASSERT_EQ(db, result.firestore());
}
Expand Down
7 changes: 7 additions & 0 deletions firestore/integration_test_internal/src/field_value_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -367,5 +367,12 @@ TEST_F(FieldValueTest, TestIncrementChoosesTheCorrectType) {
// clang-format on
}

TEST_F(FieldValueTest, TestArenaRefMinimunLimit) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to TestArenaRefMinimunLimit to mention the global refs issue that it's checking and include a link to the issue that it's verifying doesn't regress (i.e. firebase/firebase-unity-sdk#569)

std::vector<FieldValue> numbers;
for (size_t i = 0U; i < 60000; i++) {
numbers.push_back(FieldValue::Integer(i));
}
}

} // namespace firestore
} // namespace firebase
278 changes: 278 additions & 0 deletions firestore/integration_test_internal/src/jni/arena_ref_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "firestore/src/jni/arena_ref.h"
#include "firestore/src/android/firestore_android.h"
#include "firestore/src/common/make_unique.h"
#include "firestore/src/jni/loader.h"
#include "firestore/src/jni/long.h"

#include "firestore_integration_test.h"

#include "gtest/gtest.h"

namespace firebase {
namespace firestore {
namespace jni {
namespace {

constexpr char kException[] = "java/lang/Exception";
Method<String> kConstructor("<init>", "(Ljava/lang/String;)V");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Rename kConstructor to kExceptionConstructor to be clear from its name what it's the constructor for.


class ArenaRefTestAndroid : public FirestoreIntegrationTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArenaRefTestAndroid should extend FirestoreAndroidIntegrationTest, not FirestoreIntegrationTest.

This looks after everything that you are re-implementing in this class:

  1. It defines a Loader for you to use.
  2. It checks for pending exceptions at the end of the test.
  3. It provides a method to create an exception.

In fact, I think you should just go back to using ArenaRefTestAndroid = FirestoreAndroidIntegrationTest;

public:
ArenaRefTestAndroid() : loader_(app()), env_(make_unique<Env>(GetEnv())) {
loader_.LoadClass(kException);
loader_.Load(kConstructor);
}

~ArenaRefTestAndroid() override {
// Ensure that after the test is done that any pending exception is cleared
// to prevent spurious errors in the teardown of FirestoreIntegrationTest.
env_->ExceptionClear();
}

Env& env() const { return *env_; }

void throwException() {
Local<Class> clazz = env().FindClass(kException);
jmethodID ctor =
env().GetMethodId(clazz, "<init>", "(Ljava/lang/String;)V");

Local<String> message = env().NewStringUtf("Testing throw");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just get rid of these throwException() and clearExceptionOccurred() helper methods.

throwException() can be replaced by

env.Throw(CreateException(env, "Test Exception"));

and clearExceptionOccurred() can be replaced by

env.ExceptionClear();

Admittedly, this leads to a tiny bit of code duplciation between test cases but that's acceptable because, IMO, it improves readabililty of the test cases.

See go/tott/598 about "DAMP" tests rather than the "DRY" principle that is common in non-testing code.

Local<Throwable> exception = env().New<Throwable>(clazz, ctor, message);
// After throwing, use EXPECT rather than ASSERT to ensure that the
// exception is cleared.
env().Throw(exception);
EXPECT_FALSE(env().ok());
}

void clearExceptionOccurred() {
Local<Throwable> thrown = env().ClearExceptionOccurred();
EXPECT_EQ(thrown.GetMessage(env()), "Testing throw");
}

private:
// Env is declared as having a `noexcept(false)` destructor, which causes the
// compiler to propagate this into `EnvTest`'s destructor, but this conflicts
// with the declaration of the parent class. Holding `Env` with a unique
// pointer sidesteps this restriction.
std::unique_ptr<Env> env_;
Loader loader_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hold an Env in an instance variable. Instead, just do Env env when you need one. And methods of this class that need one should take one as a non-const reference.

};

TEST_F(ArenaRefTestAndroid, DefaultConstructor) {
ArenaRef arena_ref;
EXPECT_EQ(arena_ref.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, ConstructsFromNull) {
Local<String> string;
ArenaRef arena_ref(env(), string);
EXPECT_EQ(arena_ref.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, ConstructsFromValid) {
Local<String> string = env().NewStringUtf("hello world");
ArenaRef arena_ref(env(), string);
EXPECT_TRUE(env().IsSameObject(arena_ref.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, CopyConstructsFromNull) {
ArenaRef arena_ref1;
ArenaRef arena_ref2(arena_ref1);
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, CopyConstructsFromValid) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref1(env(), string);
ArenaRef arena_ref2(arena_ref1);
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string));
EXPECT_TRUE(env().IsSameObject(arena_ref2.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromNullToNull) {
ArenaRef arena_ref1, arena_ref2;
arena_ref2 = arena_ref1;
EXPECT_EQ(arena_ref1.get(env()).get(), nullptr);
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromNullToValid) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref1;
ArenaRef arena_ref2(env(), string);
arena_ref2 = arena_ref1;
EXPECT_EQ(arena_ref1.get(env()).get(), nullptr);
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromValidToNull) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref1;
ArenaRef arena_ref2(env(), string);
arena_ref1 = arena_ref2;
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string));
EXPECT_TRUE(env().IsSameObject(arena_ref2.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromValidToValid) {
Local<String> string1 = env().NewStringUtf("hello world");
Local<String> string2 = env().NewStringUtf("hello earth");

ArenaRef arena_ref1(env(), string1);
ArenaRef arena_ref2(env(), string2);
arena_ref1 = arena_ref2;

EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string2));
EXPECT_TRUE(env().IsSameObject(arena_ref2.get(env()), string2));
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromNullObjectItself) {
ArenaRef arena_ref1;
arena_ref1 = arena_ref1;
EXPECT_EQ(arena_ref1.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, CopyAssignsFromValidObjectItself) {
Local<String> string1 = env().NewStringUtf("hello world");

ArenaRef arena_ref1(env(), string1);
arena_ref1 = arena_ref1;
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string1));
}

TEST_F(ArenaRefTestAndroid, MoveConstructsFromNull) {
ArenaRef arena_ref1;
ArenaRef arena_ref2(std::move(arena_ref1));
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, MoveConstructsFromValid) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref2(env(), string);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Rename arena_ref2 and arena_ref3 back to arena_ref1 and arena_ref2, respectively.

ArenaRef arena_ref3(std::move(arena_ref2));
EXPECT_TRUE(env().IsSameObject(arena_ref3.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromNullToNull) {
ArenaRef arena_ref1, arena_ref2;
arena_ref2 = std::move(arena_ref1);
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromNullToValid) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref1;
ArenaRef arena_ref2(env(), string);
arena_ref2 = std::move(arena_ref1);
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromValidToNull) {
Local<String> string = env().NewStringUtf("hello world");

ArenaRef arena_ref1;
ArenaRef arena_ref2(env(), string);
arena_ref1 = std::move(arena_ref2);
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromValidToValid) {
Local<String> string1 = env().NewStringUtf("hello world");
Local<String> string2 = env().NewStringUtf("hello earth");

ArenaRef arena_ref1(env(), string1);
ArenaRef arena_ref2(env(), string2);
arena_ref1 = std::move(arena_ref2);
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string2));
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromNullObjectItself) {
ArenaRef arena_ref1;
arena_ref1 = std::move(arena_ref1);
EXPECT_EQ(arena_ref1.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, MoveAssignsFromValidObjectItself) {
Local<String> string1 = env().NewStringUtf("hello world");

ArenaRef arena_ref1(env(), string1);
arena_ref1 = std::move(arena_ref1);
EXPECT_TRUE(env().IsSameObject(arena_ref1.get(env()), string1));
}

TEST_F(ArenaRefTestAndroid, ThrowBeforeConstruct) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can you rename the "ThrowBeforeXXX" test names to "XXXWithPendingException" or something like that? I think using the term "with pending exception" more clearly conveys what scenario the test is testing.

Local<String> string = env().NewStringUtf("hello world");
EXPECT_EQ(string.ToString(env()).size(), 11U);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these checks for the length of the string returned from NewStringUtf(). You can just assume that this is true. Applies here and elsewhere in this file.

throwException();
ArenaRef arena_ref(env(), string);
clearExceptionOccurred();
EXPECT_EQ(arena_ref.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, ThrowBeforeCopyConstruct) {
Local<String> string = env().NewStringUtf("hello world");
ArenaRef arena_ref1(env(), string);
EXPECT_EQ(arena_ref1.get(env()).ToString(env()).size(), 11U);
throwException();
ArenaRef arena_ref2(arena_ref1);
clearExceptionOccurred();
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, ThrowBeforeCopyAssignment) {
Local<String> string = env().NewStringUtf("hello world");
ArenaRef arena_ref1(env(), string);
ArenaRef arena_ref2;
EXPECT_EQ(arena_ref1.get(env()).ToString(env()).size(), 11U);
throwException();
arena_ref2 = arena_ref1;
clearExceptionOccurred();
EXPECT_EQ(arena_ref2.get(env()).get(), nullptr);
}

TEST_F(ArenaRefTestAndroid, ThrowBeforeMoveConstruct) {
Local<String> string = env().NewStringUtf("hello world");
ArenaRef arena_ref1(env(), string);
EXPECT_EQ(arena_ref1.get(env()).ToString(env()).size(), 11U);
throwException();
ArenaRef arena_ref2(std::move(arena_ref1));
clearExceptionOccurred();
EXPECT_TRUE(env().IsSameObject(arena_ref2.get(env()), string));
}

TEST_F(ArenaRefTestAndroid, ThrowBeforeMoveAssignment) {
Local<String> string = env().NewStringUtf("hello world");
ArenaRef arena_ref1(env(), string);
ArenaRef arena_ref2;
EXPECT_EQ(arena_ref1.get(env()).ToString(env()).size(), 11U);
throwException();
arena_ref2 = std::move(arena_ref1);
clearExceptionOccurred();
EXPECT_TRUE(env().IsSameObject(arena_ref2.get(env()), string));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for calling get() with a pending exception.

} // namespace
} // namespace jni
} // namespace firestore
} // namespace firebase
27 changes: 27 additions & 0 deletions firestore/integration_test_internal/src/jni/env_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ TEST_F(EnvTest, CallsVoidMethods) {
EXPECT_EQ(length, 42);
}

TEST_F(EnvTest, CallsValidArenaRef) {
Local<String> str = env().NewStringUtf("Foo");
ArenaRef arena_ref(env(), str);

Local<Class> clazz = env().FindClass("java/lang/String");
jmethodID to_lower_case =
env().GetMethodId(clazz, "toLowerCase", "()Ljava/lang/String;");

Local<Object> result = env().Call(arena_ref, Method<Object>(to_lower_case));
EXPECT_EQ("foo", result.ToString(env()));
}

TEST_F(EnvTest, GetsStaticFields) {
Local<Class> clazz = env().FindClass("java/lang/String");
jfieldID comparator = env().GetStaticFieldId(clazz, "CASE_INSENSITIVE_ORDER",
Expand Down Expand Up @@ -160,6 +172,21 @@ TEST_F(EnvTest, ToString) {
EXPECT_EQ("Foo", result);
}

TEST_F(EnvTest, IsInstanceOfChecksValidArenaRef) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Augment the IsInstanceOfChecksValidArenaRef test to also call IsInstanceOf with a different class (e.g. java.lang.Integer) such that it returns false.

Local<Class> clazz = env().FindClass("java/lang/String");
Local<String> str = env().NewStringUtf("Foo");
ArenaRef arena_ref(env(), str);
EXPECT_TRUE(env().IsInstanceOf(arena_ref, clazz));
}

TEST_F(EnvTest, IsInstanceOfChecksNullArenaRef) {
GTEST_SKIP()
<< "Skip until edge case of IsInstanceOf (b/268420201) is covered.";
Local<Class> clazz = env().FindClass("java/lang/String");
ArenaRef arena_ref;
EXPECT_FALSE(env().IsInstanceOf(arena_ref, clazz));
}

TEST_F(EnvTest, Throw) {
Local<Class> clazz = env().FindClass("java/lang/Exception");
jmethodID ctor = env().GetMethodId(clazz, "<init>", "(Ljava/lang/String;)V");
Expand Down
4 changes: 2 additions & 2 deletions firestore/src/android/document_reference_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ Future<void> DocumentReferenceInternal::Set(const MapFieldValue& data,
Env env = GetEnv();
FieldValueInternal map_value(data);
Local<Object> java_options = SetOptionsInternal::Create(env, options);
Local<Task> task = env.Call(obj_, kSet, map_value, java_options);
Local<Task> task = env.Call(obj_, kSet, map_value.ToJava(), java_options);
return promises_.NewFuture<void>(env, AsyncFn::kSet, task);
}

Future<void> DocumentReferenceInternal::Update(const MapFieldValue& data) {
Env env = GetEnv();
FieldValueInternal map_value(data);
Local<Task> task = env.Call(obj_, kUpdate, map_value);
Local<Task> task = env.Call(obj_, kUpdate, map_value.ToJava());
return promises_.NewFuture<void>(env, AsyncFn::kUpdate, task);
}

Expand Down
Loading