Open Source the first set of MediaPipe Tasks tests for Web

PiperOrigin-RevId: 493673279
This commit is contained in:
Sebastian Schmidt
2022-12-07 12:15:34 -08:00
committed by Copybara-Service
parent 3c0ddf16b4
commit 2811e0c5c8
16 changed files with 1308 additions and 46 deletions
@@ -228,6 +228,8 @@ def mediapipe_ts_library(
srcs = srcs,
visibility = visibility,
deps = deps + [
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//@types/offscreencanvas",
"@npm//@types/google-protobuf",
],
@@ -1,5 +1,6 @@
# This package contains options shared by all MediaPipe Tasks for Web.
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("//mediapipe/framework/port:build_config.bzl", "mediapipe_ts_library")
package(default_visibility = ["//mediapipe/tasks:internal"])
@@ -13,6 +14,22 @@ mediapipe_ts_library(
],
)
mediapipe_ts_library(
name = "classifier_options_test_lib",
testonly = True,
srcs = ["classifier_options.test.ts"],
deps = [
":classifier_options",
"//mediapipe/tasks/cc/components/processors/proto:classifier_options_jspb_proto",
"//mediapipe/tasks/web/core:classifier_options",
],
)
jasmine_node_test(
name = "classifier_options_test",
deps = [":classifier_options_test_lib"],
)
mediapipe_ts_library(
name = "classifier_result",
srcs = ["classifier_result.ts"],
@@ -22,6 +39,22 @@ mediapipe_ts_library(
],
)
mediapipe_ts_library(
name = "classifier_result_test_lib",
testonly = True,
srcs = ["classifier_result.test.ts"],
deps = [
":classifier_result",
"//mediapipe/framework/formats:classification_jspb_proto",
"//mediapipe/tasks/cc/components/containers/proto:classifications_jspb_proto",
],
)
jasmine_node_test(
name = "classifier_result_test",
deps = [":classifier_result_test_lib"],
)
mediapipe_ts_library(
name = "embedder_result",
srcs = ["embedder_result.ts"],
@@ -31,6 +64,21 @@ mediapipe_ts_library(
],
)
mediapipe_ts_library(
name = "embedder_result_test_lib",
testonly = True,
srcs = ["embedder_result.test.ts"],
deps = [
":embedder_result",
"//mediapipe/tasks/cc/components/containers/proto:embeddings_jspb_proto",
],
)
jasmine_node_test(
name = "embedder_result_test",
deps = [":embedder_result_test_lib"],
)
mediapipe_ts_library(
name = "embedder_options",
srcs = ["embedder_options.ts"],
@@ -40,6 +88,22 @@ mediapipe_ts_library(
],
)
mediapipe_ts_library(
name = "embedder_options_test_lib",
testonly = True,
srcs = ["embedder_options.test.ts"],
deps = [
":embedder_options",
"//mediapipe/tasks/cc/components/processors/proto:embedder_options_jspb_proto",
"//mediapipe/tasks/web/core:embedder_options",
],
)
jasmine_node_test(
name = "embedder_options_test",
deps = [":embedder_options_test_lib"],
)
mediapipe_ts_library(
name = "base_options",
srcs = [
@@ -53,3 +117,15 @@ mediapipe_ts_library(
"//mediapipe/tasks/web/core",
],
)
mediapipe_ts_library(
name = "base_options_test_lib",
testonly = True,
srcs = ["base_options.test.ts"],
deps = [":base_options"],
)
jasmine_node_test(
name = "base_options_test",
deps = [":base_options_test_lib"],
)
@@ -0,0 +1,127 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
// Placeholder for internal dependency on encodeByteArray
// Placeholder for internal dependency on trusted resource URL builder
import {convertBaseOptionsToProto} from './base_options';
describe('convertBaseOptionsToProto()', () => {
const mockBytes = new Uint8Array([0, 1, 2, 3]);
const mockBytesResult = {
modelAsset: {
fileContent: Buffer.from(mockBytes).toString('base64'),
fileName: undefined,
fileDescriptorMeta: undefined,
filePointerMeta: undefined,
},
useStreamMode: false,
acceleration: {
xnnpack: undefined,
gpu: undefined,
tflite: {},
},
};
let fetchSpy: jasmine.Spy;
beforeEach(() => {
fetchSpy = jasmine.createSpy().and.callFake(async url => {
expect(url).toEqual('foo');
return {
arrayBuffer: () => mockBytes.buffer,
} as unknown as Response;
});
global.fetch = fetchSpy;
});
it('verifies that at least one model asset option is provided', async () => {
await expectAsync(convertBaseOptionsToProto({}))
.toBeRejectedWithError(
/Either baseOptions.modelAssetPath or baseOptions.modelAssetBuffer must be set/);
});
it('verifies that no more than one model asset option is provided', async () => {
await expectAsync(convertBaseOptionsToProto({
modelAssetPath: `foo`,
modelAssetBuffer: new Uint8Array([])
}))
.toBeRejectedWithError(
/Cannot set both baseOptions.modelAssetPath and baseOptions.modelAssetBuffer/);
});
it('downloads model', async () => {
const baseOptionsProto = await convertBaseOptionsToProto({
modelAssetPath: `foo`,
});
expect(fetchSpy).toHaveBeenCalled();
expect(baseOptionsProto.toObject()).toEqual(mockBytesResult);
});
it('does not download model when bytes are provided', async () => {
const baseOptionsProto = await convertBaseOptionsToProto({
modelAssetBuffer: new Uint8Array(mockBytes),
});
expect(fetchSpy).not.toHaveBeenCalled();
expect(baseOptionsProto.toObject()).toEqual(mockBytesResult);
});
it('can enable CPU delegate', async () => {
const baseOptionsProto = await convertBaseOptionsToProto({
modelAssetBuffer: new Uint8Array(mockBytes),
delegate: 'cpu',
});
expect(baseOptionsProto.toObject()).toEqual(mockBytesResult);
});
it('can enable GPU delegate', async () => {
const baseOptionsProto = await convertBaseOptionsToProto({
modelAssetBuffer: new Uint8Array(mockBytes),
delegate: 'gpu',
});
expect(baseOptionsProto.toObject()).toEqual({
...mockBytesResult,
acceleration: {
xnnpack: undefined,
gpu: {
useAdvancedGpuApi: false,
api: 0,
allowPrecisionLoss: true,
cachedKernelPath: undefined,
serializedModelDir: undefined,
modelToken: undefined,
usage: 2,
},
tflite: undefined,
},
});
});
it('can reset delegate', async () => {
let baseOptionsProto = await convertBaseOptionsToProto({
modelAssetBuffer: new Uint8Array(mockBytes),
delegate: 'gpu',
});
// Clear backend
baseOptionsProto =
await convertBaseOptionsToProto({delegate: undefined}, baseOptionsProto);
expect(baseOptionsProto.toObject()).toEqual(mockBytesResult);
});
});
@@ -0,0 +1,114 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {ClassifierOptions as ClassifierOptionsProto} from '../../../../tasks/cc/components/processors/proto/classifier_options_pb';
import {ClassifierOptions} from '../../../../tasks/web/core/classifier_options';
import {convertClassifierOptionsToProto} from './classifier_options';
interface TestCase {
optionName: keyof ClassifierOptions;
protoName: string;
customValue: unknown;
defaultValue: unknown;
}
describe('convertClassifierOptionsToProto()', () => {
function verifyOption(
actualClassifierOptions: ClassifierOptionsProto,
expectedClassifierOptions: Record<string, unknown> = {}): void {
expect(actualClassifierOptions.toObject())
.toEqual(jasmine.objectContaining(expectedClassifierOptions));
}
const testCases: TestCase[] = [
{
optionName: 'maxResults',
protoName: 'maxResults',
customValue: 5,
defaultValue: -1
},
{
optionName: 'displayNamesLocale',
protoName: 'displayNamesLocale',
customValue: 'en',
defaultValue: 'en'
},
{
optionName: 'scoreThreshold',
protoName: 'scoreThreshold',
customValue: 0.1,
defaultValue: undefined
},
{
optionName: 'categoryAllowlist',
protoName: 'categoryAllowlistList',
customValue: ['foo'],
defaultValue: []
},
{
optionName: 'categoryDenylist',
protoName: 'categoryDenylistList',
customValue: ['bar'],
defaultValue: []
},
];
for (const testCase of testCases) {
it(`can set ${testCase.optionName}`, () => {
const classifierOptionsProto = convertClassifierOptionsToProto(
{[testCase.optionName]: testCase.customValue});
verifyOption(
classifierOptionsProto, {[testCase.protoName]: testCase.customValue});
});
it(`can clear ${testCase.optionName}`, () => {
let classifierOptionsProto = convertClassifierOptionsToProto(
{[testCase.optionName]: testCase.customValue});
verifyOption(
classifierOptionsProto, {[testCase.protoName]: testCase.customValue});
classifierOptionsProto =
convertClassifierOptionsToProto({[testCase.optionName]: undefined});
verifyOption(
classifierOptionsProto,
{[testCase.protoName]: testCase.defaultValue});
});
}
it('overwrites options', () => {
let classifierOptionsProto =
convertClassifierOptionsToProto({maxResults: 1});
verifyOption(classifierOptionsProto, {'maxResults': 1});
classifierOptionsProto = convertClassifierOptionsToProto(
{maxResults: 2}, classifierOptionsProto);
verifyOption(classifierOptionsProto, {'maxResults': 2});
});
it('merges options', () => {
let classifierOptionsProto =
convertClassifierOptionsToProto({maxResults: 1});
verifyOption(classifierOptionsProto, {'maxResults': 1});
classifierOptionsProto = convertClassifierOptionsToProto(
{displayNamesLocale: 'en'}, classifierOptionsProto);
verifyOption(
classifierOptionsProto, {'maxResults': 1, 'displayNamesLocale': 'en'});
});
});
@@ -0,0 +1,80 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {Classification, ClassificationList} from '../../../../framework/formats/classification_pb';
import {ClassificationResult, Classifications} from '../../../../tasks/cc/components/containers/proto/classifications_pb';
import {convertFromClassificationResultProto} from './classifier_result';
// The OSS JS API does not support the builder pattern.
// tslint:disable:jspb-use-builder-pattern
describe('convertFromClassificationResultProto()', () => {
it('transforms custom values', () => {
const classificationResult = new ClassificationResult();
classificationResult.setTimestampMs(1);
const classifcations = new Classifications();
classifcations.setHeadIndex(1);
classifcations.setHeadName('headName');
const classificationList = new ClassificationList();
const clasification = new Classification();
clasification.setIndex(2);
clasification.setScore(0.3);
clasification.setDisplayName('displayName');
clasification.setLabel('categoryName');
classificationList.addClassification(clasification);
classifcations.setClassificationList(classificationList);
classificationResult.addClassifications(classifcations);
const result = convertFromClassificationResultProto(classificationResult);
expect(result).toEqual({
classifications: [{
categories: [{
index: 2,
score: 0.3,
displayName: 'displayName',
categoryName: 'categoryName'
}],
headIndex: 1,
headName: 'headName'
}],
timestampMs: 1
});
});
it('transforms default values', () => {
const classificationResult = new ClassificationResult();
const classifcations = new Classifications();
const classificationList = new ClassificationList();
const clasification = new Classification();
classificationList.addClassification(clasification);
classifcations.setClassificationList(classificationList);
classificationResult.addClassifications(classifcations);
const result = convertFromClassificationResultProto(classificationResult);
expect(result).toEqual({
classifications: [{
categories: [{index: 0, score: 0, displayName: '', categoryName: ''}],
headIndex: 0,
headName: ''
}],
});
});
});
@@ -0,0 +1,93 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {EmbedderOptions as EmbedderOptionsProto} from '../../../../tasks/cc/components/processors/proto/embedder_options_pb';
import {EmbedderOptions} from '../../../../tasks/web/core/embedder_options';
import {convertEmbedderOptionsToProto} from './embedder_options';
interface TestCase {
optionName: keyof EmbedderOptions;
protoName: string;
customValue: unknown;
defaultValue: unknown;
}
describe('convertEmbedderOptionsToProto()', () => {
function verifyOption(
actualEmbedderOptions: EmbedderOptionsProto,
expectedEmbedderOptions: Record<string, unknown> = {}): void {
expect(actualEmbedderOptions.toObject())
.toEqual(jasmine.objectContaining(expectedEmbedderOptions));
}
const testCases: TestCase[] = [
{
optionName: 'l2Normalize',
protoName: 'l2Normalize',
customValue: true,
defaultValue: undefined
},
{
optionName: 'quantize',
protoName: 'quantize',
customValue: true,
defaultValue: undefined
},
];
for (const testCase of testCases) {
it(`can set ${testCase.optionName}`, () => {
const embedderOptionsProto = convertEmbedderOptionsToProto(
{[testCase.optionName]: testCase.customValue});
verifyOption(
embedderOptionsProto, {[testCase.protoName]: testCase.customValue});
});
it(`can clear ${testCase.optionName}`, () => {
let embedderOptionsProto = convertEmbedderOptionsToProto(
{[testCase.optionName]: testCase.customValue});
verifyOption(
embedderOptionsProto, {[testCase.protoName]: testCase.customValue});
embedderOptionsProto =
convertEmbedderOptionsToProto({[testCase.optionName]: undefined});
verifyOption(
embedderOptionsProto, {[testCase.protoName]: testCase.defaultValue});
});
}
it('overwrites options', () => {
let embedderOptionsProto =
convertEmbedderOptionsToProto({l2Normalize: true});
verifyOption(embedderOptionsProto, {'l2Normalize': true});
embedderOptionsProto = convertEmbedderOptionsToProto(
{l2Normalize: false}, embedderOptionsProto);
verifyOption(embedderOptionsProto, {'l2Normalize': false});
});
it('replaces options', () => {
let embedderOptionsProto = convertEmbedderOptionsToProto({quantize: true});
verifyOption(embedderOptionsProto, {'quantize': true});
embedderOptionsProto = convertEmbedderOptionsToProto(
{l2Normalize: true}, embedderOptionsProto);
verifyOption(embedderOptionsProto, {'l2Normalize': true, 'quantize': true});
});
});
@@ -0,0 +1,75 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {Embedding, EmbeddingResult, FloatEmbedding, QuantizedEmbedding} from '../../../../tasks/cc/components/containers/proto/embeddings_pb';
import {convertFromEmbeddingResultProto} from './embedder_result';
// The OSS JS API does not support the builder pattern.
// tslint:disable:jspb-use-builder-pattern
describe('convertFromEmbeddingResultProto()', () => {
it('transforms custom values', () => {
const embedding = new Embedding();
embedding.setHeadIndex(1);
embedding.setHeadName('headName');
const floatEmbedding = new FloatEmbedding();
floatEmbedding.setValuesList([0.1, 0.9]);
embedding.setFloatEmbedding(floatEmbedding);
const resultProto = new EmbeddingResult();
resultProto.addEmbeddings(embedding);
resultProto.setTimestampMs(1);
const embedderResult = convertFromEmbeddingResultProto(resultProto);
const embeddings = embedderResult.embeddings;
const timestampMs = embedderResult.timestampMs;
expect(embeddings.length).toEqual(1);
expect(embeddings[0])
.toEqual(
{floatEmbedding: [0.1, 0.9], headIndex: 1, headName: 'headName'});
expect(timestampMs).toEqual(1);
});
it('transforms custom quantized values', () => {
const embedding = new Embedding();
embedding.setHeadIndex(1);
embedding.setHeadName('headName');
const quantizedEmbedding = new QuantizedEmbedding();
const quantizedValues = new Uint8Array([1, 2, 3]);
quantizedEmbedding.setValues(quantizedValues);
embedding.setQuantizedEmbedding(quantizedEmbedding);
const resultProto = new EmbeddingResult();
resultProto.addEmbeddings(embedding);
resultProto.setTimestampMs(1);
const embedderResult = convertFromEmbeddingResultProto(resultProto);
const embeddings = embedderResult.embeddings;
const timestampMs = embedderResult.timestampMs;
expect(embeddings.length).toEqual(1);
expect(embeddings[0]).toEqual({
quantizedEmbedding: new Uint8Array([1, 2, 3]),
headIndex: 1,
headName: 'headName'
});
expect(timestampMs).toEqual(1);
});
});
@@ -1,4 +1,5 @@
load("//mediapipe/framework/port:build_config.bzl", "mediapipe_ts_library")
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
package(default_visibility = ["//mediapipe/tasks:internal"])
@@ -9,3 +10,18 @@ mediapipe_ts_library(
"//mediapipe/tasks/web/components/containers:embedding_result",
],
)
mediapipe_ts_library(
name = "cosine_similarity_test_lib",
testonly = True,
srcs = ["cosine_similarity.test.ts"],
deps = [
":cosine_similarity",
"//mediapipe/tasks/web/components/containers:embedding_result",
],
)
jasmine_node_test(
name = "cosine_similarity_test",
deps = [":cosine_similarity_test_lib"],
)
@@ -0,0 +1,85 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* <p>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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.
*/
import {Embedding} from '../../../../tasks/web/components/containers/embedding_result';
import {computeCosineSimilarity} from './cosine_similarity';
describe('computeCosineSimilarity', () => {
it('fails with quantized and float embeddings', () => {
const u: Embedding = {floatEmbedding: [1.0], headIndex: 0, headName: ''};
const v: Embedding = {
quantizedEmbedding: new Uint8Array([1.0]),
headIndex: 0,
headName: ''
};
expect(() => computeCosineSimilarity(u, v))
.toThrowError(
/Cannot compute cosine similarity between quantized and float embeddings/);
});
it('fails with zero norm', () => {
const u = {floatEmbedding: [0.0], headIndex: 0, headName: ''};
expect(() => computeCosineSimilarity(u, u))
.toThrowError(
/Cannot compute cosine similarity on embedding with 0 norm/);
});
it('fails with different sizes', () => {
const u:
Embedding = {floatEmbedding: [1.0, 2.0], headIndex: 0, headName: ''};
const v: Embedding = {
floatEmbedding: [1.0, 2.0, 3.0],
headIndex: 0,
headName: ''
};
expect(() => computeCosineSimilarity(u, v))
.toThrowError(
/Cannot compute cosine similarity between embeddings of different sizes/);
});
it('succeeds with float embeddings', () => {
const u: Embedding = {
floatEmbedding: [1.0, 0.0, 0.0, 0.0],
headIndex: 0,
headName: ''
};
const v: Embedding = {
floatEmbedding: [0.5, 0.5, 0.5, 0.5],
headIndex: 0,
headName: ''
};
expect(computeCosineSimilarity(u, v)).toEqual(0.5);
});
it('succeeds with quantized embeddings', () => {
const u: Embedding = {
quantizedEmbedding: new Uint8Array([255, 128, 128, 128]),
headIndex: 0,
headName: ''
};
const v: Embedding = {
quantizedEmbedding: new Uint8Array([0, 128, 128, 128]),
headIndex: 0,
headName: ''
};
expect(computeCosineSimilarity(u, v)).toEqual(-1.0);
});
});
+33
View File
@@ -1,6 +1,7 @@
# This package contains options shared by all MediaPipe Tasks for Web.
load("//mediapipe/framework/port:build_config.bzl", "mediapipe_ts_declaration", "mediapipe_ts_library")
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
package(default_visibility = ["//mediapipe/tasks:internal"])
@@ -32,6 +33,38 @@ mediapipe_ts_library(
deps = [":core"],
)
mediapipe_ts_library(
name = "task_runner_test_utils",
testonly = True,
srcs = [
"task_runner_test_utils.ts",
],
deps = [
"//mediapipe/framework:calculator_jspb_proto",
"//mediapipe/web/graph_runner:graph_runner_ts",
"//mediapipe/web/graph_runner:register_model_resources_graph_service_ts",
],
)
mediapipe_ts_library(
name = "task_runner_test_lib",
testonly = True,
srcs = [
"task_runner_test.ts",
],
deps = [
":task_runner",
":task_runner_test_utils",
"//mediapipe/tasks/cc/core/proto:base_options_jspb_proto",
"//mediapipe/web/graph_runner:graph_runner_ts",
],
)
jasmine_node_test(
name = "task_runner_test",
deps = [":task_runner_test_lib"],
)
mediapipe_ts_declaration(
name = "classifier_options",
srcs = ["classifier_options.d.ts"],
+4 -3
View File
@@ -77,9 +77,10 @@ export abstract class TaskRunner {
}
constructor(
wasmModule: WasmModule,
glCanvas?: HTMLCanvasElement|OffscreenCanvas|null) {
this.graphRunner = new GraphRunnerImageLib(wasmModule, glCanvas);
wasmModule: WasmModule, glCanvas?: HTMLCanvasElement|OffscreenCanvas|null,
graphRunner?: GraphRunnerImageLib) {
this.graphRunner =
graphRunner ?? new GraphRunnerImageLib(wasmModule, glCanvas);
// Disables the automatic render-to-screen code, which allows for pure
// CPU processing.
@@ -0,0 +1,107 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {BaseOptions as BaseOptionsProto} from '../../../tasks/cc/core/proto/base_options_pb';
import {TaskRunner} from '../../../tasks/web/core/task_runner';
import {createSpyWasmModule, SpyWasmModule} from '../../../tasks/web/core/task_runner_test_utils';
import {ErrorListener} from '../../../web/graph_runner/graph_runner';
import {GraphRunnerImageLib} from './task_runner';
class TaskRunnerFake extends TaskRunner {
protected baseOptions = new BaseOptionsProto();
private errorListener: ErrorListener|undefined;
private errors: string[] = [];
static createFake(): TaskRunnerFake {
const wasmModule = createSpyWasmModule();
return new TaskRunnerFake(wasmModule);
}
constructor(wasmModuleFake: SpyWasmModule) {
super(
wasmModuleFake, /* glCanvas= */ null,
jasmine.createSpyObj<GraphRunnerImageLib>([
'setAutoRenderToScreen', 'setGraph', 'finishProcessing',
'registerModelResourcesGraphService', 'attachErrorListener'
]));
const graphRunner = this.graphRunner as jasmine.SpyObj<GraphRunnerImageLib>;
expect(graphRunner.registerModelResourcesGraphService).toHaveBeenCalled();
expect(graphRunner.setAutoRenderToScreen).toHaveBeenCalled();
graphRunner.attachErrorListener.and.callFake(listener => {
this.errorListener = listener;
});
graphRunner.setGraph.and.callFake(() => {
this.throwErrors();
});
graphRunner.finishProcessing.and.callFake(() => {
this.throwErrors();
});
}
enqueueError(message: string): void {
this.errors.push(message);
}
override finishProcessing(): void {
super.finishProcessing();
}
override setGraph(graphData: Uint8Array, isBinary: boolean): void {
super.setGraph(graphData, isBinary);
}
private throwErrors(): void {
expect(this.errorListener).toBeDefined();
for (const error of this.errors) {
this.errorListener!(/* errorCode= */ -1, error);
}
this.errors = [];
}
}
describe('TaskRunner', () => {
it('handles errors during graph update', () => {
const taskRunner = TaskRunnerFake.createFake();
taskRunner.enqueueError('Test error');
expect(() => {
taskRunner.setGraph(new Uint8Array(0), /* isBinary= */ true);
}).toThrowError('Test error');
});
it('handles errors during graph execution', () => {
const taskRunner = TaskRunnerFake.createFake();
taskRunner.setGraph(new Uint8Array(0), /* isBinary= */ true);
taskRunner.enqueueError('Test error');
expect(() => {
taskRunner.finishProcessing();
}).toThrowError('Test error');
});
it('can handle multiple errors', () => {
const taskRunner = TaskRunnerFake.createFake();
taskRunner.enqueueError('Test error 1');
taskRunner.enqueueError('Test error 2');
expect(() => {
taskRunner.setGraph(new Uint8Array(0), /* isBinary= */ true);
}).toThrowError(/Test error 1, Test error 2/);
});
});
@@ -0,0 +1,113 @@
/**
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
*
* 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.
*/
import 'jasmine';
import {CalculatorGraphConfig} from '../../../framework/calculator_pb';
import {WasmModule} from '../../../web/graph_runner/graph_runner';
import {WasmModuleRegisterModelResources} from '../../../web/graph_runner/register_model_resources_graph_service';
type SpyWasmModuleInternal = WasmModule&WasmModuleRegisterModelResources;
/**
* Convenience type for our fake WasmModule for Jasmine testing.
*/
export declare type SpyWasmModule = jasmine.SpyObj<SpyWasmModuleInternal>;
/**
* Factory function for creating a fake WasmModule for our Jasmine tests,
* allowing our APIs to no longer rely on the Wasm layer so they can run tests
* in pure JS/TS (and optionally spy on the calls).
*/
export function createSpyWasmModule(): SpyWasmModule {
return jasmine.createSpyObj<SpyWasmModuleInternal>([
'_setAutoRenderToScreen', 'stringToNewUTF8', '_attachProtoListener',
'_attachProtoVectorListener', '_free', '_waitUntilIdle',
'_addStringToInputStream', '_registerModelResourcesGraphService',
'_configureAudio'
]);
}
/**
* Sets up our equality testing to use a custom float equality checking function
* to avoid incorrect test results due to minor floating point inaccuracies.
*/
export function addJasmineCustomFloatEqualityTester() {
jasmine.addCustomEqualityTester((a, b) => { // Custom float equality
if (a === +a && b === +b && (a !== (a | 0) || b !== (b | 0))) {
return Math.abs(a - b) < 5e-8;
}
return;
});
}
/** The minimum interface provided by a test fake. */
export interface MediapipeTasksFake {
graph: CalculatorGraphConfig|undefined;
calculatorName: string;
attachListenerSpies: jasmine.Spy[];
}
/** An map of field paths to values */
export type FieldPathToValue = [string[] | string, unknown];
/**
* Verifies that the graph has been initialized and that it contains the
* provided options.
*/
export function verifyGraph(
tasksFake: MediapipeTasksFake,
expectedCalculatorOptions?: FieldPathToValue,
expectedBaseOptions?: FieldPathToValue,
): void {
expect(tasksFake.graph).toBeDefined();
expect(tasksFake.graph!.getNodeList().length).toBe(1);
const node = tasksFake.graph!.getNodeList()[0].toObject();
expect(node).toEqual(
jasmine.objectContaining({calculator: tasksFake.calculatorName}));
if (expectedBaseOptions) {
const [fieldPath, value] = expectedBaseOptions;
let proto = (node.options as {ext: {baseOptions: unknown}}).ext.baseOptions;
for (const fieldName of (
Array.isArray(fieldPath) ? fieldPath : [fieldPath])) {
proto = ((proto ?? {}) as Record<string, unknown>)[fieldName];
}
expect(proto).toEqual(value);
}
if (expectedCalculatorOptions) {
const [fieldPath, value] = expectedCalculatorOptions;
let proto = (node.options as {ext: unknown}).ext;
for (const fieldName of (
Array.isArray(fieldPath) ? fieldPath : [fieldPath])) {
proto = ((proto ?? {}) as Record<string, unknown>)[fieldName];
}
expect(proto).toEqual(value);
}
}
/**
* Verifies all listeners (as exposed by `.attachListenerSpies`) have been
* attached at least once since the last call to `verifyListenersRegistered()`.
* This helps us to ensure that listeners are re-registered with every graph
* update.
*/
export function verifyListenersRegistered(tasksFake: MediapipeTasksFake): void {
for (const spy of tasksFake.attachListenerSpies) {
expect(spy.calls.count()).toBeGreaterThanOrEqual(1);
spy.calls.reset();
}
}