blob: 52b5cc704d77eb2e16b8096d33758a3b7cf28280 [file] [log] [blame]
/*
* 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.
*/
package com.android.car.scriptexecutor;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.UserHandle;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorConstants;
import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(JUnit4.class)
public final class ScriptExecutorTest {
private IScriptExecutor mScriptExecutor;
private ScriptExecutor mInstance;
private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
public PersistableBundle mSavedBundle;
public PersistableBundle mFinalResult;
public int mErrorType;
public String mMessage;
public String mStackTrace;
public final CountDownLatch mResponseLatch = new CountDownLatch(1);
@Override
public void onScriptFinished(PersistableBundle result) {
mFinalResult = result;
mResponseLatch.countDown();
}
@Override
public void onSuccess(PersistableBundle stateToPersist) {
mSavedBundle = stateToPersist;
mResponseLatch.countDown();
}
@Override
public void onError(int errorType, String message, String stackTrace) {
mErrorType = errorType;
mMessage = message;
mStackTrace = stackTrace;
mResponseLatch.countDown();
}
}
private final ScriptExecutorListener mFakeScriptExecutorListener =
new ScriptExecutorListener();
private final PersistableBundle mPublishedData = new PersistableBundle();
private final PersistableBundle mSavedState = new PersistableBundle();
private final CountDownLatch mBindLatch = new CountDownLatch(1);
private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
private static final int SCRIPT_PROCESSING_TIMEOUT_SEC = 10;
private final ServiceConnection mScriptExecutorConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
mBindLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName className) {
fail("Service unexpectedly disconnected");
}
};
// Helper method to invoke the script and wait for it to complete and return a response.
private void runScriptAndWaitForResponse(String script, String function,
PersistableBundle publishedData, PersistableBundle previousState)
throws RemoteException {
mScriptExecutor.invokeScript(script, function, publishedData, previousState,
mFakeScriptExecutorListener);
try {
if (!mFakeScriptExecutorListener.mResponseLatch.await(SCRIPT_PROCESSING_TIMEOUT_SEC,
TimeUnit.SECONDS)) {
fail("Failed to get the callback method called by the script on time");
}
} catch (InterruptedException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
private void runScriptAndWaitForError(String script, String function) throws RemoteException {
runScriptAndWaitForResponse(script, function, new PersistableBundle(),
new PersistableBundle());
}
@Before
public void setUp() throws InterruptedException {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.car.scriptexecutor",
"com.android.car.scriptexecutor.ScriptExecutor"));
mContext.bindServiceAsUser(intent, mScriptExecutorConnection, Context.BIND_AUTO_CREATE,
UserHandle.SYSTEM);
if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
fail("Failed to bind to ScriptExecutor service");
}
}
@Test
public void invokeScript_returnsResult() throws RemoteException {
String returnResultScript =
"function hello(data, state)\n"
+ " result = {hello=\"world\"}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(returnResultScript, "hello", mPublishedData, 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_allSupportedPrimitiveTypes() throws RemoteException {
String script =
"function knows(data, state)\n"
+ " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "knows", mPublishedData, 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.getLong("integer")).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
}
@Test
public void invokeScript_skipsUnsupportedTypes() throws RemoteException {
String script =
"function nested(data, 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";
runScriptAndWaitForResponse(script, "nested", mPublishedData, mSavedState);
// Verify that expected error is received.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).contains(
"nested tables are not supported");
}
@Test
public void invokeScript_emptyBundle() throws RemoteException {
String script =
"function empty(data, state)\n"
+ " result = {}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "empty", mPublishedData, 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.
String script =
"function update(data, state)\n"
+ " result = {y = state.x+1}\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
previousState.putInt("x", 1);
runScriptAndWaitForResponse(script, "update", mPublishedData, previousState);
// Verify that y = 2, because y = x + 1 and x = 1.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("y")).isEqualTo(2);
}
@Test
public void invokeScript_allSupportedPrimitiveTypesWorkRoundTripWithKeyNamesPreserved()
throws RemoteException {
// Here we verify that all supported primitive types in supplied previous state Bundle
// are interpreted by the script as expected.
String script =
"function update_all(data, 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";
PersistableBundle previousState = new PersistableBundle();
previousState.putInt("integer", 1);
previousState.putDouble("number", 0.1);
previousState.putBoolean("boolean", false);
previousState.putString("string", "ABRA");
runScriptAndWaitForResponse(script, "update_all", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("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");
}
@Test
public void invokeScript_allSupportedArrayTypesWorkRoundTripWithKeyNamesPreserved()
throws RemoteException {
// Here we verify that all supported array types in supplied previous state Bundle are
// interpreted by the script as expected.
String script =
"function arrays(data, state)\n"
+ " result = {}\n"
+ " result.int_array = state.int_array\n"
+ " result.long_array = state.long_array\n"
+ " result.string_array = state.string_array\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
int[] int_array = new int[]{1, 2};
long[] int_array_in_long = new long[]{1, 2};
long[] long_array = new long[]{1, 2, 3};
String[] string_array = new String[]{"one", "two", "three"};
previousState.putIntArray("int_array", int_array);
previousState.putLongArray("long_array", long_array);
previousState.putStringArray("string_array", string_array);
runScriptAndWaitForResponse(script, "arrays", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(3);
// Lua has only one lua_Integer. Here Java long is used to represent it when data is
// transferred from Lua to CarTelemetryService.
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("int_array")).isEqualTo(
int_array_in_long);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
long_array);
assertThat(
mFakeScriptExecutorListener.mSavedBundle.getStringArray("string_array")).isEqualTo(
string_array);
}
@Test
public void invokeScript_modifiesArray()
throws RemoteException {
// Verify that an array modified by a script is properly sent back by the callback.
String script =
"function modify_array(data, state)\n"
+ " result = {}\n"
+ " result.long_array = state.long_array\n"
+ " result.long_array[2] = 100\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
long[] long_array = new long[]{1, 2, 3};
previousState.putLongArray("long_array", long_array);
long[] expected_array = new long[]{1, 100, 3};
runScriptAndWaitForResponse(script, "modify_array", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
expected_array);
}
@Test
public void invokeScript_processesStringArray()
throws RemoteException {
// Verify that an array modified by a script is properly sent back by the callback.
String script =
"function process_string_array(data, state)\n"
+ " result = {}\n"
+ " result.answer = state.string_array[1] .. state.string_array[2]\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
String[] string_array = new String[]{"Hello ", "world!"};
previousState.putStringArray("string_array", string_array);
runScriptAndWaitForResponse(script, "process_string_array", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("answer")).isEqualTo(
"Hello world!");
}
@Test
public void invokeScript_arraysWithLengthAboveLimitCauseError()
throws RemoteException {
// Verifies that arrays pushed by Lua that have their size over the limit cause error.
String script =
"function size_limit(data, state)\n"
+ " result = {}\n"
+ " result.huge_array = {}\n"
+ " for i=1, 10000 do\n"
+ " result.huge_array[i]=i\n"
+ " end\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "size_limit", mPublishedData, mSavedState);
// Verify that expected error is received.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"Returned table huge_array exceeds maximum allowed size of 1000 "
+ "elements. This key-value cannot be unpacked successfully. This error "
+ "is unrecoverable.");
}
@Test
public void invokeScript_arrayContainingVaryingTypesCausesError()
throws RemoteException {
// Verifies that values in returned array must be the same integer type.
// For example string values in a Lua array are not allowed.
String script =
"function table_with_numbers_and_strings(data, state)\n"
+ " result = {}\n"
+ " result.mixed_array = state.long_array\n"
+ " result.mixed_array[2] = 'a'\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
long[] long_array = new long[]{1, 2, 3};
previousState.putLongArray("long_array", long_array);
runScriptAndWaitForResponse(script, "table_with_numbers_and_strings", mPublishedData,
previousState);
// Verify that expected error is received.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).contains(
"Returned Lua arrays must have elements of the same type.");
}
@Test
public void invokeScript_InTablesWithBothKeysAndIndicesCopiesOnlyIndexedData()
throws RemoteException {
// Documents the current behavior that copies only indexed values in a Lua table that
// contains both keyed and indexed data.
String script =
"function keys_and_indices(data, state)\n"
+ " result = {}\n"
+ " result.mixed_array = state.long_array\n"
+ " result.mixed_array['a'] = 130\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
long[] long_array = new long[]{1, 2, 3};
previousState.putLongArray("long_array", long_array);
runScriptAndWaitForResponse(script, "keys_and_indices", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("mixed_array")).isEqualTo(
long_array);
}
@Test
public void invokeScript_noLuaBufferOverflowForLargeInputArrays() throws RemoteException {
// Tests that arrays with length that exceed internal Lua buffer size of 20 elements
// do not cause buffer overflow and are handled properly.
String script =
"function large_input_array(data, state)\n"
+ " sum = 0\n"
+ " for _, val in ipairs(state.long_array) do\n"
+ " sum = sum + val\n"
+ " end\n"
+ " result = {total = sum}\n"
+ " on_success(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
int n = 10000;
long[] longArray = new long[n];
for (int i = 0; i < n; i++) {
longArray[i] = i;
}
previousState.putLongArray("long_array", longArray);
long expected_sum =
(longArray[0] + longArray[n - 1]) * n / 2; // sum of an arithmetic sequence.
runScriptAndWaitForResponse(script, "large_input_array", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("total")).isEqualTo(
expected_sum);
}
@Test
public void invokeScript_scriptCallsOnError() throws RemoteException {
String script =
"function calls_on_error()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(\"one is not equal to two\")\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "calls_on_error");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo("one is not equal to two");
}
@Test
public void invokeScript_tooManyParametersInOnError() throws RemoteException {
String script =
"function too_many_params_in_on_error()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(\"param1\", \"param2\")\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "too_many_params_in_on_error");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_error can push only a single string parameter from Lua");
}
@Test
public void invokeScript_onErrorOnlyAcceptsString() throws RemoteException {
String script =
"function only_string()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(false)\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "only_string");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_error can push only a single string parameter from Lua");
}
@Test
public void invokeScript_returnsFinalResult() throws RemoteException {
String returnFinalResultScript =
"function script_finishes(data, state)\n"
+ " result = {data = state.input + 1}\n"
+ " on_script_finished(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
previousState.putInt("input", 1);
runScriptAndWaitForResponse(returnFinalResultScript, "script_finishes", mPublishedData,
previousState);
// Expect to get back a bundle with a single key-value pair {"data": 2}
// because data = state.input + 1 as in the script body above.
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("data")).isEqualTo(2);
}
@Test
public void invokeScript_allPrimitiveSupportedTypesForReturningFinalResult()
throws RemoteException {
// Here we verify that all supported primitive types are present in the returned final
// result bundle are present.
String script =
"function finalize_all(data, 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_script_finished(result)\n"
+ "end\n";
PersistableBundle previousState = new PersistableBundle();
previousState.putInt("integer", 1);
previousState.putDouble("number", 0.1);
previousState.putBoolean("boolean", false);
previousState.putString("string", "ABRA");
runScriptAndWaitForResponse(script, "finalize_all", mPublishedData, previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(4);
assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("integer")).isEqualTo(2);
assertThat(mFakeScriptExecutorListener.mFinalResult.getDouble("number")).isEqualTo(0.2);
assertThat(mFakeScriptExecutorListener.mFinalResult.getBoolean("boolean")).isEqualTo(true);
assertThat(mFakeScriptExecutorListener.mFinalResult.getString("string")).isEqualTo(
"ABRACADABRA");
}
@Test
public void invokeScript_emptyFinalResultBundle() throws RemoteException {
String script =
"function empty_final_result(data, state)\n"
+ " result = {}\n"
+ " on_script_finished(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "empty_final_result", mPublishedData, mSavedState);
// If a script returns empty table as the final result, we get an empty bundle.
assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(0);
}
@Test
public void invokeScript_wrongNumberOfCallbackInputsInOnScriptFinished()
throws RemoteException {
String script =
"function wrong_number_of_outputs_in_on_script_finished(data, state)\n"
+ " result = {}\n"
+ " extra = 1\n"
+ " on_script_finished(result, extra)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_script_finished",
mPublishedData, mSavedState);
// We expect to get an error here because we expect only 1 input parameter in
// on_script_finished.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_script_finished can push only a single parameter from Lua - a Lua table");
}
@Test
public void invokeScript_wrongNumberOfCallbackInputsInOnSuccess() throws RemoteException {
String script =
"function wrong_number_of_outputs_in_on_success(data, state)\n"
+ " result = {}\n"
+ " extra = 1\n"
+ " on_success(result, extra)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_success",
mPublishedData, mSavedState);
// We expect to get an error here because we expect only 1 input parameter in on_success.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_success can push only a single parameter from Lua - a Lua table");
}
@Test
public void invokeScript_wrongTypeInOnSuccess() throws RemoteException {
String script =
"function wrong_type_in_on_success(data, state)\n"
+ " result = 1\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "wrong_type_in_on_success",
mPublishedData, mSavedState);
// We expect to get an error here because the type of the input parameter for on_success
// must be a Lua table.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_success can push only a single parameter from Lua - a Lua table");
}
@Test
public void invokeScript_wrongTypeInOnScriptFinished() throws RemoteException {
String script =
"function wrong_type_in_on_script_finished(data, state)\n"
+ " result = 1\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResponse(script, "wrong_type_in_on_script_finished",
mPublishedData, mSavedState);
// We expect to get an error here because the type of the input parameter for
// on_script_finished must be a Lua table.
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_success can push only a single parameter from Lua - a Lua table");
}
@Test
public void invokeScriptLargeInput_largePublishedData() throws Exception {
// Verifies that large input does not overwhelm Binder's buffer because pipes are used
// instead.
String script =
"function large_published_data(data, state)\n"
+ " sum = 0\n"
+ " for _, val in ipairs(data.array) do\n"
+ " sum = sum + val\n"
+ " end\n"
+ " result = {total = sum}\n"
+ " on_script_finished(result)\n"
+ "end\n";
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor writeFd = fds[1];
ParcelFileDescriptor readFd = fds[0];
PersistableBundle bundle = new PersistableBundle();
int n = 1 << 20; // 1024 * 1024 values, roughly 1 Million.
long[] array8Mb = new long[n];
for (int i = 0; i < n; i++) {
array8Mb[i] = i;
}
bundle.putLongArray("array", array8Mb);
long expectedSum =
(array8Mb[0] + array8Mb[n - 1]) * n / 2; // sum of an arithmetic sequence.
mScriptExecutor.invokeScriptForLargeInput(script, "large_published_data", readFd,
mSavedState,
mFakeScriptExecutorListener);
readFd.close();
try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd)) {
bundle.writeToStream(outputStream);
}
boolean gotResponse = mFakeScriptExecutorListener.mResponseLatch.await(
SCRIPT_PROCESSING_TIMEOUT_SEC,
TimeUnit.SECONDS);
assertWithMessage("Failed to get the callback method called by the script on time")
.that(gotResponse).isTrue();
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("total"))
.isEqualTo(expectedSum);
}
@Test
public void invokeScript_bothPublishedDataAndPreviousStateAreProvided() throws RemoteException {
// Verifies that both published data and previous state PersistableBundles
// are piped into script.
String script =
"function data_and_state(data, state)\n"
+ " result = {answer = data.a .. data.b .. state.c .. state.d}\n"
+ " on_script_finished(result)\n"
+ "end\n";
PersistableBundle publishedData = new PersistableBundle();
publishedData.putString("a", "A");
publishedData.putString("b", "B");
PersistableBundle previousState = new PersistableBundle();
previousState.putString("c", "C");
previousState.putString("d", "D");
runScriptAndWaitForResponse(script, "data_and_state", publishedData, previousState);
// If a script returns empty table as the final result, we get an empty bundle.
assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mFinalResult.getString("answer")).isEqualTo(
"ABCD");
}
@Test
public void invokeScript_outputIntAndLongAreTreatedAsLong() throws RemoteException {
// Verifies that we treat output both integer and long as long integer type although we
// distinguish between int and long in the script input.
String script =
"function int_and_long_are_output_long(data, state)\n"
+ " result = {int = data.int, long = state.long}\n"
+ " on_script_finished(result)\n"
+ "end\n";
PersistableBundle publishedData = new PersistableBundle();
publishedData.putInt("int", 100);
PersistableBundle previousState = new PersistableBundle();
previousState.putLong("long", 200);
runScriptAndWaitForResponse(script, "int_and_long_are_output_long",
publishedData, previousState);
// If a script returns empty table as the final result, we get an empty bundle.
assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(2);
// getInt should always return "empty" value (zero) because all integer types are treated
// as Java long.
assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("int")).isEqualTo(0);
assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("long")).isEqualTo(0);
// Instead all expected integer values are successfully retrieved using getLong method
// from the output bundle.
assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("int")).isEqualTo(100);
assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("long")).isEqualTo(200);
}
}