add harness for ohos ArrayBuffer

This commit is contained in:
guoling
2026-10-16 15:43:58 +08:00
parent b8ec9e75e7
commit 9cee61ee48
8 changed files with 222 additions and 3 deletions
+2
View File
@@ -3,6 +3,8 @@
This is a bugfix release based on v2.4.2, focused on encryption, expiration, imports, and file recovery. It also adds Android KMP initialization without an explicit Context and non-expired-key enumeration on Apple platforms.
**Upcoming compatibility change:** The release after v2.4.3 will raise the minimum supported versions from iOS 13.0 to **iOS 15.0** and from watchOS 6.0 to **watchOS 9.0**, to align with Xcode 27's supported deployment targets. The minimum deployment targets in v2.4.3 remain unchanged.
### Changes for All Platforms
* **Fix:** Prevented IV reuse when repeatedly updating an encrypted store containing a single key.
* **Fix:** Loaded expiration state before the first read or write after reopening a store, and honored expiration in read-only reads, key lists, and counts.
+1 -1
View File
@@ -68,5 +68,5 @@ recover
itemSizeLimit
-keep-dts
/Users/lingol/Developer/mmkv/OpenHarmony/MMKV/src/main/cpp/types/libmmkv/
/Users/guoling/Developer/mmkv/OpenHarmony/MMKV/src/main/cpp/types/libmmkv/
@@ -16,3 +16,29 @@ Coverage includes malformed numeric ranges and wrong value types, N-API failures
typed element size and actual pointer alignment, missing arguments, optional expiration dispatch, empty slices,
zero-copy pointers for valid nonempty slices, no write/preservation on rejection for all five encoders,
and empty/nonempty output ArrayBuffers (including a null backing pointer for zero bytes).
The complementary device harness is `entry/src/ohosTest/ets/test/ArrayBufferBounds.test.ets` (relative to
`OpenHarmony`). Its 40 Hypium cases exercise the real N-API callbacks and MMKV storage for all five encoders:
valid nonzero offsets, invalid numeric ranges and bounds, typed alignment, preservation after rejection,
omitted/undefined expiration, expiration-enabled writes, empty buffers, negative zero, and public typed subviews.
It creates unique stores under `filesDir/mmkv_arraybuffer_bounds` and removes only those stores after testing.
Wrong argument types, simulated N-API failures, and null backing pointers remain covered by the portable harness.
With DevEco's Node/Hvigor/HDC on `PATH`, `DEVECO_SDK_HOME` configured, and an existing debug signing configuration
valid for the attached physical device, build from `OpenHarmony`:
```sh
ohpm install
hvigorw --mode module -p module=entry@ohosTest -p product=default -p buildMode=debug assembleHap --no-daemon
```
The demo's main module must already be installed with the same signing identity. Select the physical device's
serial explicitly, then install the test module without uninstalling or clearing the demo:
```sh
hdc -t "$DEVICE_SERIAL" install -r entry/build/default/outputs/ohosTest/entry-ohosTest-signed.hap
hdc -t "$DEVICE_SERIAL" shell aa test -b com.tencent.mmkvdemo -m entry_test \
-s unittest OpenHarmonyTestRunner -s class ArrayBufferBounds -s timeout 15000
```
Check the reported test count and failures; a successful HDC exit alone does not establish that the suite passed.
+4 -1
View File
@@ -9,5 +9,8 @@
"@tencent/mmkv": "file:../MMKV",
// "@tencent/mmkv": "~2.4.3",
"libentry.so": "file:./src/main/cpp/types/libentry",
},
"devDependencies": {
"libmmkv.so": "file:../MMKV/src/main/cpp/types/libmmkv"
}
}
}
@@ -0,0 +1,182 @@
/*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* Licensed under the BSD 3-Clause License.
*/
import { abilityDelegatorRegistry } from '@kit.TestKit';
import { beforeAll, beforeEach, afterAll, describe, expect, it } from '@ohos/hypium';
import { MMKV, MMKVLogLevel } from '@tencent/mmkv';
import native from 'libmmkv.so';
interface SliceCodec {
name: string;
elementSize: number;
encode: (handle: bigint, key: string, value: ArrayBuffer, offset: number, length: number,
expiration?: number) => boolean;
decode: (handle: bigint, key: string, defaultValue?: ArrayBuffer) => ArrayBuffer;
}
function assertBytes(actual: ArrayBuffer, expected: Uint8Array): void {
expect(actual instanceof ArrayBuffer).assertTrue();
const bytes = new Uint8Array(actual);
expect(bytes.length).assertEqual(expected.length);
for (let index = 0; index < expected.length; index++) {
expect(bytes[index]).assertEqual(expected[index]);
}
}
export default function arrayBufferBoundsTest(): void {
describe('ArrayBufferBounds', () => {
const codecs: SliceCodec[] = [
{ name: 'bytes', elementSize: 1, encode: native.encodeBytes, decode: native.decodeBytes },
{ name: 'int32', elementSize: 4, encode: native.encodeInt32Array, decode: native.decodeInt32Array },
{ name: 'uint32', elementSize: 4, encode: native.encodeUInt32Array, decode: native.decodeUInt32Array },
{ name: 'int64', elementSize: 8, encode: native.encodeInt64Array, decode: native.decodeInt64Array },
{ name: 'uint64', elementSize: 8, encode: native.encodeUInt64Array, decode: native.decodeUInt64Array }
];
const source = new Uint8Array(24);
for (let index = 0; index < source.length; index++) {
source[index] = index + 1;
}
const buffer = source.buffer as ArrayBuffer;
const fallback = new Uint8Array([255, 254, 253, 252, 251, 250, 249, 248]).buffer as ArrayBuffer;
const key = 'slice';
let handle: bigint = 0n;
let mmapID: string = '';
let rootDir: string = '';
beforeAll(() => {
const context = abilityDelegatorRegistry.getAbilityDelegator().getAppContext();
rootDir = context.filesDir + '/mmkv_arraybuffer_bounds';
MMKV.initializeWithPath(rootDir, context.cacheDir, MMKVLogLevel.LevelWarning);
mmapID = 'arraybuffer_bounds_' + Date.now().toString() + '_' + Math.random().toString().slice(2);
handle = native.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, undefined, rootDir);
expect(handle !== 0n).assertTrue();
});
beforeEach(() => {
expect(handle !== 0n).assertTrue();
native.clearAll(handle, false);
expect(native.disableAutoKeyExpire(handle)).assertTrue();
});
afterAll(() => {
if (handle !== 0n) {
native.close(handle);
handle = 0n;
expect(native.removeStorage(mmapID, rootDir)).assertTrue();
}
});
const seed = (codec: SliceCodec): void => {
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2)).assertTrue();
expect(native.containsKey(handle, key)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
};
const reject = (codec: SliceCodec, offset: number, length: number, expiration?: number): void => {
expect(codec.encode(handle, key, buffer, offset, length, expiration)).assertFalse();
expect(native.containsKey(handle, key)).assertTrue();
expect(native.count(handle, false) === 1n).assertTrue();
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
};
codecs.forEach((codec: SliceCodec) => {
it(codec.name + '_valid_nonzero_offset', 0, () => {
seed(codec); // Five arguments: the optional expiration is genuinely omitted.
native.clearMemoryCache(handle);
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
});
it(codec.name + '_explicit_undefined_expiration', 0, () => {
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2, undefined)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
});
it(codec.name + '_invalid_numbers_preserve_value', 0, () => {
seed(codec);
const invalid: number[] = [-1, 0.5, NaN, Infinity, -Infinity, 4294967295, 4294967296, 4294967297];
invalid.forEach((value: number) => {
reject(codec, value, codec.elementSize * 2);
reject(codec, codec.elementSize, value);
});
});
it(codec.name + '_out_of_bounds_preserve_value', 0, () => {
seed(codec);
reject(codec, buffer.byteLength + 1, 0);
reject(codec, buffer.byteLength, codec.elementSize);
reject(codec, buffer.byteLength - codec.elementSize, codec.elementSize * 2);
reject(codec, 0, buffer.byteLength + codec.elementSize);
});
if (codec.elementSize > 1) {
it(codec.name + '_misaligned_slices_preserve_value', 0, () => {
seed(codec);
reject(codec, 1, codec.elementSize);
reject(codec, 0, codec.elementSize + 1);
});
}
it(codec.name + '_empty_slices_roundtrip', 0, () => {
[0, 1, buffer.byteLength].forEach((offset: number) => {
seed(codec);
expect(codec.encode(handle, key, buffer, offset, 0)).assertTrue();
expect(native.containsKey(handle, key)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
});
expect(codec.encode(handle, key, new ArrayBuffer(0), 0, 0)).assertTrue();
expect(native.containsKey(handle, key)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
});
it(codec.name + '_negative_zero_is_valid', 0, () => {
expect(codec.encode(handle, key, buffer, -0, codec.elementSize)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), source.subarray(0, codec.elementSize));
});
it(codec.name + '_expiration_path_preserves_value_on_rejection', 0, () => {
expect(native.enableAutoKeyExpire(handle, 0)).assertTrue();
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2, 3600)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
reject(codec, -1, codec.elementSize, 3600);
reject(codec, buffer.byteLength, codec.elementSize, 3600);
expect(codec.encode(handle, key, new ArrayBuffer(0), 0, 0, 3600)).assertTrue();
expect(native.containsKey(handle, key)).assertTrue();
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
});
});
it('public_typed_subviews_roundtrip', 0, () => {
const wrapperID = mmapID + '_public';
const kv = MMKV.mmkvWithID(wrapperID, MMKV.SINGLE_PROCESS_MODE, undefined, rootDir);
try {
const int32 = new Int32Array([-99, -2147483648, 2147483647, 99]);
const uint32 = new Uint32Array([99, 0, 4294967295, 99]);
const int64 = new BigInt64Array([-99n, -9223372036854775808n, 9223372036854775807n, 99n]);
const uint64 = new BigUint64Array([99n, 0n, 18446744073709551615n, 99n]);
expect(kv.encodeTypedArray('int32', int32.subarray(1, 3))).assertTrue();
expect(kv.encodeTypedArray('uint32', uint32.subarray(1, 3))).assertTrue();
expect(kv.encodeTypedArray('int64', int64.subarray(1, 3))).assertTrue();
expect(kv.encodeTypedArray('uint64', uint64.subarray(1, 3))).assertTrue();
const int32Result = kv.decodeInt32Array('int32', new Int32Array(1))!;
const uint32Result = kv.decodeUint32Array('uint32', new Uint32Array(1))!;
const int64Result = kv.decodeInt64Array('int64', new BigInt64Array(1))!;
const uint64Result = kv.decodeUint64Array('uint64', new BigUint64Array(1))!;
expect(int32Result.length).assertEqual(2);
expect(uint32Result.length).assertEqual(2);
expect(int64Result.length).assertEqual(2);
expect(uint64Result.length).assertEqual(2);
for (let index = 0; index < 2; index++) {
expect(int32Result[index]).assertEqual(int32[index + 1]);
expect(uint32Result[index]).assertEqual(uint32[index + 1]);
expect(int64Result[index] === int64[index + 1]).assertTrue();
expect(uint64Result[index] === uint64[index + 1]).assertTrue();
}
} finally {
kv.close();
expect(MMKV.removeStorage(wrapperID, rootDir)).assertTrue();
}
});
});
}
@@ -19,7 +19,9 @@
*/
import abilityTest from './Ability.test';
import arrayBufferBoundsTest from './ArrayBufferBounds.test';
export default function testsuite() {
abilityTest();
}
arrayBufferBoundsTest();
}
@@ -72,6 +72,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:MMKVDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
@@ -90,6 +91,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:MMKVDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
@@ -70,6 +70,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:MMKVDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
@@ -86,6 +87,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:MMKVDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>