Merge "Introduce ability for the script to send the results back." into sc-v2-dev
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
index 6eaec62..9d8a05b 100644
--- a/cpp/telemetry/script_executor/Android.bp
+++ b/cpp/telemetry/script_executor/Android.bp
@@ -34,6 +34,7 @@
"scriptexecutor_defaults",
],
srcs: [
+ "src/BundleWrapper.cpp",
"src/JniUtils.cpp",
"src/LuaEngine.cpp",
"src/ScriptExecutorListener.cpp",
diff --git a/cpp/telemetry/script_executor/src/BundleWrapper.cpp b/cpp/telemetry/script_executor/src/BundleWrapper.cpp
new file mode 100644
index 0000000..77d2a5f
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/BundleWrapper.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * 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 "BundleWrapper.h"
+
+#include <android-base/logging.h>
+#include <android_runtime/AndroidRuntime.h>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+BundleWrapper::BundleWrapper(JNIEnv* env) {
+ mJNIEnv = env;
+ mBundleClass =
+ static_cast<jclass>(mJNIEnv->NewGlobalRef(mJNIEnv->FindClass("android/os/Bundle")));
+ jmethodID bundleConstructor = mJNIEnv->GetMethodID(mBundleClass, "<init>", "()V");
+ mBundle = mJNIEnv->NewGlobalRef(mJNIEnv->NewObject(mBundleClass, bundleConstructor));
+}
+
+BundleWrapper::~BundleWrapper() {
+ // Delete global JNI references.
+ if (mBundle != NULL) {
+ mJNIEnv->DeleteGlobalRef(mBundle);
+ }
+ if (mBundleClass != NULL) {
+ mJNIEnv->DeleteGlobalRef(mBundleClass);
+ }
+}
+
+void BundleWrapper::putBoolean(const char* key, bool value) {
+ jmethodID putBooleanMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putBoolean", "(Ljava/lang/String;Z)V");
+ mJNIEnv->CallVoidMethod(mBundle, putBooleanMethod, mJNIEnv->NewStringUTF(key),
+ static_cast<jboolean>(value));
+}
+
+void BundleWrapper::putInteger(const char* key, int value) {
+ jmethodID putIntMethod = mJNIEnv->GetMethodID(mBundleClass, "putInt", "(Ljava/lang/String;I)V");
+ mJNIEnv->CallVoidMethod(mBundle, putIntMethod, mJNIEnv->NewStringUTF(key),
+ static_cast<jint>(value));
+}
+
+void BundleWrapper::putDouble(const char* key, double value) {
+ jmethodID putDoubleMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putDouble", "(Ljava/lang/String;D)V");
+ mJNIEnv->CallVoidMethod(mBundle, putDoubleMethod, mJNIEnv->NewStringUTF(key),
+ static_cast<jdouble>(value));
+}
+
+void BundleWrapper::putString(const char* key, const char* value) {
+ jmethodID putStringMethod = mJNIEnv->GetMethodID(mBundleClass, "putString",
+ "(Ljava/lang/String;Ljava/lang/String;)V");
+ mJNIEnv->CallVoidMethod(mBundle, putStringMethod, mJNIEnv->NewStringUTF(key),
+ mJNIEnv->NewStringUTF(value));
+}
+
+jobject BundleWrapper::getBundle() {
+ return mBundle;
+}
+
+} // namespace script_executor
+} // namespace telemetry
+} // namespace automotive
+} // namespace android
diff --git a/cpp/telemetry/script_executor/src/BundleWrapper.h b/cpp/telemetry/script_executor/src/BundleWrapper.h
new file mode 100644
index 0000000..8c42c46
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/BundleWrapper.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
+#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
+
+#include "jni.h"
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+// Used to create a java bundle object and populate its fields one at a time.
+class BundleWrapper {
+public:
+ explicit BundleWrapper(JNIEnv* env);
+ // BundleWrapper is not copyable.
+ BundleWrapper(const BundleWrapper&) = delete;
+ BundleWrapper& operator=(const BundleWrapper&) = delete;
+
+ virtual ~BundleWrapper();
+
+ // Family of methods that puts the provided 'value' into the Bundle under provided 'key'.
+ void putBoolean(const char* key, bool value);
+ void putInteger(const char* key, int value);
+ void putDouble(const char* key, double value);
+ void putString(const char* key, const char* value);
+
+ jobject getBundle();
+
+private:
+ // The class asks Java to create Bundle object and stores the reference.
+ // When the instance of this class is destroyed the actual Java Bundle object behind
+ // this reference stays on and is managed by Java.
+ jobject mBundle;
+
+ // Reference to java Bundle class cached for performance reasons.
+ jclass mBundleClass;
+
+ // Stores a JNIEnv* pointer.
+ JNIEnv* mJNIEnv; // not owned
+};
+
+} // namespace script_executor
+} // namespace telemetry
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
diff --git a/cpp/telemetry/script_executor/src/JniUtils.cpp b/cpp/telemetry/script_executor/src/JniUtils.cpp
index 93c1af8..cfe1da4 100644
--- a/cpp/telemetry/script_executor/src/JniUtils.cpp
+++ b/cpp/telemetry/script_executor/src/JniUtils.cpp
@@ -21,8 +21,8 @@
namespace telemetry {
namespace script_executor {
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
- lua_newtable(luaEngine->GetLuaState());
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
+ lua_newtable(luaEngine->getLuaState());
// null bundle object is allowed. We will treat it as an empty table.
if (bundle == nullptr) {
return;
@@ -62,19 +62,19 @@
if (env->IsInstanceOf(value, booleanClass)) {
jmethodID boolMethod = env->GetMethodID(booleanClass, "booleanValue", "()Z");
bool boolValue = static_cast<bool>(env->CallBooleanMethod(value, boolMethod));
- lua_pushboolean(luaEngine->GetLuaState(), boolValue);
+ lua_pushboolean(luaEngine->getLuaState(), boolValue);
} else if (env->IsInstanceOf(value, integerClass)) {
jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
- lua_pushinteger(luaEngine->GetLuaState(), env->CallIntMethod(value, intMethod));
+ lua_pushinteger(luaEngine->getLuaState(), env->CallIntMethod(value, intMethod));
} else if (env->IsInstanceOf(value, numberClass)) {
// Condense other numeric types using one class. Because lua supports only
// integer or double, and we handled integer in previous if clause.
jmethodID numberMethod = env->GetMethodID(numberClass, "doubleValue", "()D");
/* Pushes a double onto the stack */
- lua_pushnumber(luaEngine->GetLuaState(), env->CallDoubleMethod(value, numberMethod));
+ lua_pushnumber(luaEngine->getLuaState(), env->CallDoubleMethod(value, numberMethod));
} else if (env->IsInstanceOf(value, stringClass)) {
const char* rawStringValue = env->GetStringUTFChars((jstring)value, nullptr);
- lua_pushstring(luaEngine->GetLuaState(), rawStringValue);
+ lua_pushstring(luaEngine->getLuaState(), rawStringValue);
env->ReleaseStringUTFChars((jstring)value, rawStringValue);
} else {
// Other types are not implemented yet, skipping.
@@ -84,7 +84,7 @@
const char* rawKey = env->GetStringUTFChars(key, nullptr);
// table[rawKey] = value, where value is on top of the stack,
// and the table is the next element in the stack.
- lua_setfield(luaEngine->GetLuaState(), /* idx= */ -2, rawKey);
+ lua_setfield(luaEngine->getLuaState(), /* idx= */ -2, rawKey);
env->ReleaseStringUTFChars(key, rawKey);
}
}
diff --git a/cpp/telemetry/script_executor/src/JniUtils.h b/cpp/telemetry/script_executor/src/JniUtils.h
index c3ef677..85034d7 100644
--- a/cpp/telemetry/script_executor/src/JniUtils.h
+++ b/cpp/telemetry/script_executor/src/JniUtils.h
@@ -29,7 +29,7 @@
// converted to the corresponding key-value pairs of the Lua table as long as
// the Bundle value types are supported. At this point, we support boolean,
// integer, double and String types in Java.
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
} // namespace script_executor
} // namespace telemetry
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
index 1a074f2..e2bcef7 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ b/cpp/telemetry/script_executor/src/LuaEngine.cpp
@@ -16,10 +16,15 @@
#include "LuaEngine.h"
+#include "BundleWrapper.h"
+
+#include <android-base/logging.h>
+
#include <utility>
extern "C" {
#include "lauxlib.h"
+#include "lua.h"
#include "lualib.h"
}
@@ -28,6 +33,16 @@
namespace telemetry {
namespace script_executor {
+namespace {
+
+enum LuaNumReturnedResults {
+ ZERO_RETURNED_RESULTS = 0,
+};
+
+} // namespace
+
+ScriptExecutorListener* LuaEngine::sListener = nullptr;
+
LuaEngine::LuaEngine() {
// Instantiate Lua environment
mLuaState = luaL_newstate();
@@ -38,15 +53,18 @@
lua_close(mLuaState);
}
-lua_State* LuaEngine::GetLuaState() {
+lua_State* LuaEngine::getLuaState() {
return mLuaState;
}
-void LuaEngine::ResetListener(ScriptExecutorListener* listener) {
- mListener.reset(listener);
+void LuaEngine::resetListener(ScriptExecutorListener* listener) {
+ if (sListener != nullptr) {
+ delete sListener;
+ }
+ sListener = listener;
}
-int LuaEngine::LoadScript(const char* scriptBody) {
+int LuaEngine::loadScript(const char* scriptBody) {
// As the first step in Lua script execution we want to load
// the body of the script into Lua stack and have it processed by Lua
// to catch any errors.
@@ -61,11 +79,15 @@
// Starting read about Lua stack: https://www.lua.org/pil/24.2.html
// TODO(b/192284232): add test case to trigger this.
lua_pop(mLuaState, 1);
+ return status;
}
+
+ // Register limited set of reserved methods for Lua to call native side.
+ lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
return status;
}
-bool LuaEngine::PushFunction(const char* functionName) {
+bool LuaEngine::pushFunction(const char* functionName) {
// Interaction between native code and Lua happens via Lua stack.
// In such model, a caller first pushes the name of the function
// that needs to be called, followed by the function's input
@@ -78,7 +100,7 @@
return status;
}
-int LuaEngine::Run() {
+int LuaEngine::run() {
// Performs blocking call of the provided Lua function. Assumes all
// input arguments are in the Lua stack as well in proper order.
// On how to call Lua functions: https://www.lua.org/pil/25.2.html
@@ -89,6 +111,54 @@
return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
}
+int LuaEngine::onSuccess(lua_State* lua) {
+ if (sListener == nullptr) {
+ LOG(FATAL) << "sListener object must not be null";
+ }
+ // Any script we run can call on_success only with a single argument of Lua table type.
+ if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
+ // TODO(b/193565932): Return programming error through binder callback interface.
+ LOG(ERROR) << "Only a single input argument, a Lua table object, expected here";
+ }
+
+ // Helper object to create and populate Java Bundle object.
+ BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+ // Iterate over Lua table which is expected to be at the top of Lua stack.
+ // lua_next call pops the key from the top of the stack and finds the next
+ // key-value pair for the popped key. It returns 0 if the next pair was not found.
+ // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
+ lua_pushnil(lua); // First key is a null value.
+ while (lua_next(lua, /* index =*/-2) != 0) {
+ // 'key' is at index -2 and 'value' is at index -1
+ // -1 index is the top of the stack.
+ // remove 'value' and keep 'key' for next iteration
+ // Process each key-value depending on a type and push it to Java Bundle.
+ const char* key = lua_tostring(lua, /* index =*/-2);
+ if (lua_isboolean(lua, /* index =*/-1)) {
+ bundleWrapper.putBoolean(key, static_cast<bool>(lua_toboolean(lua, /* index =*/-1)));
+ } else if (lua_isinteger(lua, /* index =*/-1)) {
+ bundleWrapper.putInteger(key, static_cast<int>(lua_tointeger(lua, /* index =*/-1)));
+ } else if (lua_isnumber(lua, /* index =*/-1)) {
+ bundleWrapper.putDouble(key, static_cast<double>(lua_tonumber(lua, /* index =*/-1)));
+ } else if (lua_isstring(lua, /* index =*/-1)) {
+ bundleWrapper.putString(key, lua_tostring(lua, /* index =*/-1));
+ } else {
+ // not supported yet...
+ LOG(WARNING) << "key=" << key << " has a Lua type which is not supported yet. "
+ << "The bundle object will not have this key-value pair.";
+ }
+ // Pop 1 element from the stack.
+ lua_pop(lua, 1);
+ // The key is at index -1, the table is at index -2 now.
+ }
+
+ // Forward the populated Bundle object to Java callback.
+ sListener->onSuccess(bundleWrapper.getBundle());
+ // We explicitly must tell Lua how many results we return, which is 0 in this case.
+ // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ return ZERO_RETURNED_RESULTS;
+}
+
} // namespace script_executor
} // namespace telemetry
} // namespace automotive
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.h b/cpp/telemetry/script_executor/src/LuaEngine.h
index a1d6e48..39f3a9f 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ b/cpp/telemetry/script_executor/src/LuaEngine.h
@@ -38,30 +38,47 @@
virtual ~LuaEngine();
// Returns pointer to Lua state object.
- lua_State* GetLuaState();
+ lua_State* getLuaState();
// Loads Lua script provided as scriptBody string.
// Returns 0 if successful. Otherwise returns non-zero Lua error code.
- int LoadScript(const char* scriptBody);
+ int loadScript(const char* scriptBody);
// Pushes a Lua function under provided name into the stack.
// Returns true if successful.
- bool PushFunction(const char* functionName);
+ bool pushFunction(const char* functionName);
// Invokes function with the inputs provided in the stack.
- // Assumes that the script body has been already loaded and successully
+ // Assumes that the script body has been already loaded and successfully
// compiled and run, and all input arguments, and the function have been
// pushed to the stack.
// Returns 0 if successful. Otherwise returns non-zero Lua error code.
- int Run();
+ int run();
// Updates stored listener and destroys the previous one.
- void ResetListener(ScriptExecutorListener* listener);
+ static void resetListener(ScriptExecutorListener* listener);
private:
- lua_State* mLuaState; // owned
+ // Invoked by running Lua script to store intermediate results.
+ // The script will provide the results as a Lua table.
+ // We currently support only non-nested fields in the table and the fields can be the following
+ // Lua types: boolean, number, integer, and string.
+ // The result is converted to Android Bundle and forwarded to
+ // ScriptExecutor service via callback interface.
+ static int onSuccess(lua_State* lua);
- std::unique_ptr<ScriptExecutorListener> mListener;
+ // Points to the current listener object.
+ // Lua cannot call non-static class methods. We need to access listener object instance in
+ // Lua callbacks. Therefore, callbacks callable by Lua are static class methods and the pointer
+ // to a listener object needs to be static, since static methods cannot access non-static
+ // members.
+ // Only one listener is supported at any given time.
+ // Since listeners are heap-allocated, the destructor does not need to run at shutdown
+ // of the service because the memory allocated to the current listener object will be
+ // reclaimed by the OS.
+ static ScriptExecutorListener* sListener;
+
+ lua_State* mLuaState; // owned
};
} // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
index 500b8e2..0bdc692 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
@@ -65,9 +65,8 @@
// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
//
-// Finally, once parsing and pushing to Lua stack is complete, we do
-//
-// Step 6: attempt to run the provided function.
+// Finally, once parsing and pushing to Lua stack is complete, we go on to the final step,
+// Step 6: Attempt to run the provided function.
JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeInvokeScript(
JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
jobject publishedData, jobject savedState, jobject listener) {
@@ -88,7 +87,7 @@
// Load and parse the script
const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
- auto status = engine->LoadScript(scriptStr);
+ auto status = engine->loadScript(scriptStr);
env->ReleaseStringUTFChars(scriptBody, scriptStr);
// status == 0 if the script loads successfully.
if (status) {
@@ -96,11 +95,11 @@
"Failed to load the script.");
return;
}
- engine->ResetListener(new ScriptExecutorListener(env, listener));
+ LuaEngine::resetListener(new ScriptExecutorListener(env, listener));
// Push the function name we want to invoke to Lua stack
const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
- status = engine->PushFunction(functionNameStr);
+ status = engine->pushFunction(functionNameStr);
env->ReleaseStringUTFChars(functionName, functionNameStr);
// status == 1 if the name is indeed a function.
if (!status) {
@@ -119,10 +118,10 @@
// Unpack bundle in savedState, convert to Lua table and push it to Lua
// stack.
- PushBundleToLuaTable(env, engine, savedState);
+ pushBundleToLuaTable(env, engine, savedState);
// Execute the function. This will block until complete or error.
- if (engine->Run()) {
+ if (engine->run()) {
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
"Runtime error occurred while running the function.");
return;
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
index 8c10aa4..d80aae8 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
@@ -17,7 +17,6 @@
#include "ScriptExecutorListener.h"
#include <android-base/logging.h>
-#include <android_runtime/AndroidRuntime.h>
namespace android {
namespace automotive {
@@ -25,14 +24,27 @@
namespace script_executor {
ScriptExecutorListener::~ScriptExecutorListener() {
- if (mScriptExecutorListener != NULL) {
- JNIEnv* env = AndroidRuntime::getJNIEnv();
+ JNIEnv* env = getCurrentJNIEnv();
+ if (mScriptExecutorListener != nullptr) {
env->DeleteGlobalRef(mScriptExecutorListener);
}
}
ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
+ env->GetJavaVM(&mJavaVM);
+}
+
+void ScriptExecutorListener::onSuccess(jobject bundle) {
+ JNIEnv* env = getCurrentJNIEnv();
+ if (mScriptExecutorListener == nullptr) {
+ env->FatalError(
+ "mScriptExecutorListener must point to a valid listener object, not nullptr.");
+ }
+ jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
+ jmethodID onSuccessMethod =
+ env->GetMethodID(listenerClass, "onSuccess", "(Landroid/os/Bundle;)V");
+ env->CallVoidMethod(mScriptExecutorListener, onSuccessMethod, bundle);
}
void ScriptExecutorListener::onError(const int errorType, const std::string& message,
@@ -41,6 +53,14 @@
<< ", stackTrace: " << stackTrace;
}
+JNIEnv* ScriptExecutorListener::getCurrentJNIEnv() {
+ JNIEnv* env;
+ if (mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ LOG(FATAL) << "Unable to return JNIEnv from JavaVM";
+ }
+ return env;
+}
+
} // namespace script_executor
} // namespace telemetry
} // namespace automotive
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
index 1e5c7d7..5f6c380 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
@@ -35,13 +35,20 @@
void onScriptFinished() {}
- void onSuccess() {}
+ void onSuccess(jobject bundle);
void onError(const int errorType, const std::string& message, const std::string& stackTrace);
+ JNIEnv* getCurrentJNIEnv();
+
private:
// Stores a jni global reference to Java Script Executor listener object.
jobject mScriptExecutorListener;
+
+ // Stores JavaVM pointer in order to be able to get JNIEnv pointer.
+ // This is done because JNIEnv cannot be shared between threads.
+ // https://developer.android.com/training/articles/perf-jni.html#javavm-and-jnienv
+ JavaVM* mJavaVM;
};
} // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
index 9e2c43a..8cdf87a 100644
--- a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
+++ b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
@@ -44,21 +44,21 @@
JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativePushBundleToLuaTableCaller(
JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
- PushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
+ pushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
bundle);
}
JNIEXPORT jint JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeGetObjectSize(
JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- return lua_rawlen(engine->GetLuaState(), static_cast<int>(index));
+ return lua_rawlen(engine->getLuaState(), static_cast<int>(index));
}
JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasBooleanValue(
JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
const char* rawKey = env->GetStringUTFChars(key, nullptr);
LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- auto* luaState = engine->GetLuaState();
+ auto* luaState = engine->getLuaState();
lua_pushstring(luaState, rawKey);
env->ReleaseStringUTFChars(key, rawKey);
lua_gettable(luaState, -2);
@@ -76,7 +76,7 @@
const char* rawKey = env->GetStringUTFChars(key, nullptr);
LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
// Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
+ auto* luaState = engine->getLuaState();
lua_pushstring(luaState, rawKey);
env->ReleaseStringUTFChars(key, rawKey);
lua_gettable(luaState, -2);
@@ -94,7 +94,7 @@
const char* rawKey = env->GetStringUTFChars(key, nullptr);
LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
// Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
+ auto* luaState = engine->getLuaState();
lua_pushstring(luaState, rawKey);
env->ReleaseStringUTFChars(key, rawKey);
lua_gettable(luaState, -2);
@@ -112,7 +112,7 @@
const char* rawKey = env->GetStringUTFChars(key, nullptr);
LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
// Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
+ auto* luaState = engine->getLuaState();
lua_pushstring(luaState, rawKey);
env->ReleaseStringUTFChars(key, rawKey);
lua_gettable(luaState, -2);
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
index 774da25..344fb04 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
@@ -16,6 +16,8 @@
package com.android.car.telemetry;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.fail;
import android.car.telemetry.IScriptExecutor;
@@ -47,12 +49,17 @@
private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+ public Bundle mSavedBundle;
+ public final CountDownLatch mSuccessLatch = new CountDownLatch(1);
+
@Override
public void onScriptFinished(byte[] result) {
}
@Override
public void onSuccess(Bundle stateToPersist) {
+ mSavedBundle = stateToPersist;
+ mSuccessLatch.countDown();
}
@Override
@@ -60,7 +67,7 @@
}
}
- private final IScriptExecutorListener mFakeScriptExecutorListener =
+ private final ScriptExecutorListener mFakeScriptExecutorListener =
new ScriptExecutorListener();
// TODO(b/189241508). Parsing of publishedData is not implemented yet.
@@ -69,15 +76,16 @@
private final Bundle mSavedState = new Bundle();
private static final String LUA_SCRIPT =
- "function hello(data, state)\n"
- + " print(\"Hello World\")\n"
- + "end\n";
+ "function hello(state)\n"
+ + " print(\"Hello World\")\n"
+ + "end\n";
private static final String LUA_METHOD = "hello";
private final CountDownLatch mBindLatch = new CountDownLatch(1);
private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
+ private static final int SCRIPT_SUCCESS_TIMEOUT_SEC = 10;
private final ServiceConnection mScriptExecutorConnection =
new ServiceConnection() {
@@ -93,6 +101,22 @@
}
};
+ // Helper method to invoke the script and wait for it to complete and return the result.
+ public void runScriptAndWaitForResult(String script, String function, Bundle previousState)
+ throws RemoteException {
+ mScriptExecutor.invokeScript(script, function, mPublishedData, previousState,
+ mFakeScriptExecutorListener);
+ try {
+ if (!mFakeScriptExecutorListener.mSuccessLatch.await(SCRIPT_SUCCESS_TIMEOUT_SEC,
+ TimeUnit.SECONDS)) {
+ fail("Failed to get on_success called by the script on time");
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+
@Before
public void setUp() throws InterruptedException {
mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
@@ -119,5 +143,129 @@
fail(e.getMessage());
}
}
+
+ @Test
+ public void invokeScript_returnsResult() throws RemoteException {
+ String returnResultScript =
+ "function hello(state)\n"
+ + " result = {hello=\"world\"}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+
+ runScriptAndWaitForResult(returnResultScript, "hello", mSavedState);
+
+ // Expect to get back a bundle with a single string key: string value pair:
+ // {"hello": "world"}
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("hello")).isEqualTo("world");
+ }
+
+ @Test
+ public void invokeScript_allSupportedTypes() throws RemoteException {
+ String script =
+ "function knows(state)\n"
+ + " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+
+ runScriptAndWaitForResult(script, "knows", mSavedState);
+
+ // Expect to get back a bundle with 4 keys, each corresponding to a distinct supported type.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo("hello");
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
+ }
+
+ @Test
+ public void invokeScript_skipsUnsupportedTypes() throws RemoteException {
+ String script =
+ "function nested(state)\n"
+ + " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ + " result.nested_table = {x=0, y=0}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+
+ runScriptAndWaitForResult(script, "nested", mSavedState);
+
+ // Bundle does not contain any value under "nested_table" key, because nested tables are
+ // not supported yet.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("nested_table")).isNull();
+ }
+
+ @Test
+ public void invokeScript_emptyBundle() throws RemoteException {
+ String script =
+ "function empty(state)\n"
+ + " result = {}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+
+ runScriptAndWaitForResult(script, "empty", mSavedState);
+
+ // If a script returns empty table as the result, we get an empty bundle.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void invokeScript_processPreviousStateAndReturnResult() throws RemoteException {
+ // Here we verify that the script actually processes provided state from a previous run
+ // and makes calculation based on that and returns the result.
+ // TODO(b/189241508): update function signatures.
+ String script =
+ "function update(state)\n"
+ + " result = {y = state.x+1}\n"
+ + " on_success(result)\n"
+ + "end\n";
+ Bundle previousState = new Bundle();
+ previousState.putInt("x", 1);
+
+
+ runScriptAndWaitForResult(script, "update", previousState);
+
+ // Verify that y = 2, because y = x + 1 and x = 1.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("y")).isEqualTo(2);
+ }
+
+ @Test
+ public void invokeScript_allSupportedTypesWorkRoundTripWithKeyNamesPreserved()
+ throws RemoteException {
+ // Here we verify that all supported types in supplied previous state Bundle are interpreted
+ // by the script as expected.
+ // TODO(b/189241508): update function signatures.
+ String script =
+ "function update_all(state)\n"
+ + " result = {}\n"
+ + " result.integer = state.integer + 1\n"
+ + " result.number = state.number + 0.1\n"
+ + " result.boolean = not state.boolean\n"
+ + " result.string = state.string .. \"CADABRA\"\n"
+ + " on_success(result)\n"
+ + "end\n";
+ Bundle previousState = new Bundle();
+ previousState.putInt("integer", 1);
+ previousState.putDouble("number", 0.1);
+ previousState.putBoolean("boolean", false);
+ previousState.putString("string", "ABRA");
+
+
+ runScriptAndWaitForResult(script, "update_all", previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(2);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(0.2);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+ "ABRACADABRA");
+ }
}