Skip to content

Commit

Permalink
Fix flaky bucket eviction tests by splitting/isolating them (#5876)
Browse files Browse the repository at this point in the history
* fix flaky bucket eviction tests by splitting/isolating them

* fix imports
  • Loading branch information
philippotto authored Nov 30, 2021
1 parent 7690ab5 commit 102b4b0
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @flow
import test from "ava";
import mockRequire from "mock-require";
import { waitForCondition } from "libs/utils";

import "test/sagas/saga_integration.mock";
import { __setupOxalis, createBucketResponseFunction } from "test/helpers/apiHelpers";
import { restartSagaAction, wkReadyAction } from "oxalis/model/actions/actions";
import Store from "oxalis/store";
import { hasRootSagaCrashed } from "oxalis/model/sagas/root_saga";

import { testLabelingManyBuckets } from "./bucket_eviction_helper";

const { discardSaveQueuesAction } = mockRequire.reRequire("oxalis/model/actions/save_actions");

test.beforeEach(async t => {
// Setup oxalis, this will execute model.fetch(...) and initialize the store with the tracing, etc.
Store.dispatch(restartSagaAction());
Store.dispatch(discardSaveQueuesAction());

await __setupOxalis(t, "volume");

// Dispatch the wkReadyAction, so the sagas are started
Store.dispatch(wkReadyAction());
});

test.serial(
"Brushing/Tracing should crash when too many buckets are labeled at once without saving inbetween",
async t => {
await t.context.api.tracing.save();

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
0,
0,
);
// webKnossos will start to evict buckets forcefully if too many are dirty at the same time.
// This is not ideal, but usually handled by the fact that buckets are regularly saved to the
// backend and then marked as not dirty.
// This test provokes that webKnossos crashes (a hard crash is only done during testing; in dev/prod
// a soft warning is emitted via the devtools).
// The corresponding sibling test checks that saving inbetween does not make webKnossos crash.
t.plan(2);
t.false(hasRootSagaCrashed());
const failedSagaPromise = waitForCondition(hasRootSagaCrashed, 500);
await Promise.race([testLabelingManyBuckets(t, false), failedSagaPromise]);
t.true(hasRootSagaCrashed());
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @flow
import test from "ava";
import mockRequire from "mock-require";
import { waitForCondition } from "libs/utils";

import "test/sagas/saga_integration.mock";
import { __setupOxalis, createBucketResponseFunction } from "test/helpers/apiHelpers";
import { restartSagaAction, wkReadyAction } from "oxalis/model/actions/actions";
import Store from "oxalis/store";
import { hasRootSagaCrashed } from "oxalis/model/sagas/root_saga";

import { testLabelingManyBuckets } from "./bucket_eviction_helper";

const { discardSaveQueuesAction } = mockRequire.reRequire("oxalis/model/actions/save_actions");

test.beforeEach(async t => {
// Setup oxalis, this will execute model.fetch(...) and initialize the store with the tracing, etc.
Store.dispatch(restartSagaAction());
Store.dispatch(discardSaveQueuesAction());

await __setupOxalis(t, "volume");

// Dispatch the wkReadyAction, so the sagas are started
Store.dispatch(wkReadyAction());
});

test.serial(
"Brushing/Tracing should not crash when too many buckets are labeled at once with saving inbetween",
async t => {
await t.context.api.tracing.save();

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
0,
0,
);
t.plan(2);
t.false(hasRootSagaCrashed());
const failedSagaPromise = waitForCondition(hasRootSagaCrashed, 500);
await Promise.race([testLabelingManyBuckets(t, true), failedSagaPromise]);
t.false(hasRootSagaCrashed());
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// @flow
import _ from "lodash";
import mockRequire from "mock-require";

import "test/sagas/saga_integration.mock";
import { createBucketResponseFunction } from "test/helpers/apiHelpers";
import Store from "oxalis/store";

import { OrthoViews, AnnotationToolEnum } from "oxalis/constants";
import { updateUserSettingAction } from "oxalis/model/actions/settings_actions";

const { setToolAction } = mockRequire.reRequire("oxalis/model/actions/ui_actions");
const { setPositionAction } = mockRequire.reRequire("oxalis/model/actions/flycam_actions");

const {
setActiveCellAction,
addToLayerAction,
startEditingAction,
finishEditingAction,
} = mockRequire.reRequire("oxalis/model/actions/volumetracing_actions");

export async function testLabelingManyBuckets(t: any, saveInbetween: boolean) {
// We set MAXIMUM_BUCKET_COUNT to 150 and then label 199 = 75 (mag1) + 124 (downsampled) buckets in total.
// In between, we will save the data which allows the buckets of the first batch to be GC'ed.
// Therefore, saving the buckets of the second batch should not cause any problems.
t.context.model.getCubeByLayerName("segmentation").MAXIMUM_BUCKET_COUNT = 150;

const oldCellId = 11;
const brushSize = 10;
const newCellId = 2;

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
oldCellId,
500,
);
// Reload buckets which might have already been loaded before swapping the sendJSONReceiveArraybufferWithHeaders
// function.
await t.context.api.data.reloadAllBuckets();

// Prepare to paint into the center of 50 buckets.
const paintPositions1 = _.range(0, 50).map(idx => [32 * idx + 16, 32 * idx + 16, 32 * idx + 16]);
// Prepare to paint into the center of 50 other buckets.
const paintPositions2 = _.range(50, 100).map(idx => [
32 * idx + 16,
32 * idx + 16,
32 * idx + 16,
]);

Store.dispatch(updateUserSettingAction("brushSize", brushSize));
Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH));
Store.dispatch(setActiveCellAction(newCellId));

for (const paintPosition of paintPositions1) {
Store.dispatch(setPositionAction(paintPosition));
Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY));
Store.dispatch(addToLayerAction(paintPosition));
Store.dispatch(finishEditingAction());
}

console.log("saveInbetween", saveInbetween);
if (saveInbetween) {
await t.context.api.tracing.save();
}

for (const paintPosition of paintPositions2) {
Store.dispatch(setPositionAction(paintPosition));
Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY));
Store.dispatch(addToLayerAction(paintPosition));
Store.dispatch(finishEditingAction());
}

await t.context.api.tracing.save();
}

export default {};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import "test/sagas/volumetracing_saga.mock.js";
import "test/sagas/volumetracing/volumetracing_saga.mock";
import sinon from "sinon";

import { take, put, call } from "redux-saga/effects";
Expand All @@ -20,8 +20,8 @@ import mockRequire from "mock-require";
import test from "ava";
import type { VolumeTracing } from "oxalis/store";

import { expectValueDeepEqual, execCall } from "../helpers/sagaHelpers";
import { withoutUpdateTracing } from "../helpers/saveHelpers";
import { expectValueDeepEqual, execCall } from "test/helpers/sagaHelpers";
import { withoutUpdateTracing } from "test/helpers/saveHelpers";

mockRequire("app", { currentUser: { firstName: "SCM", lastName: "Boy" } });
mockRequire("oxalis/model/sagas/root_saga", function*() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// @flow
/* eslint-disable no-await-in-loop */

import mockRequire from "mock-require";
import test from "ava";
import { waitForCondition } from "libs/utils";
import _ from "lodash";
import mockRequire from "mock-require";

import "test/sagas/saga_integration.mock";
import {
Expand Down Expand Up @@ -489,98 +487,3 @@ test.serial("Brushing/Tracing with undo (II)", async t => {
t.is(await t.context.api.data.getDataValue("segmentation", [1, 0, 0]), newCellId + 1);
t.is(await t.context.api.data.getDataValue("segmentation", [5, 0, 0]), oldCellId);
});

test.serial(
"Brushing/Tracing should crash when too many buckets are labeled at once without saving inbetween",
async t => {
await t.context.api.tracing.save();

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
0,
0,
);
// webKnossos will start to evict buckets forcefully if too many are dirty at the same time.
// This is not ideal, but usually handled by the fact that buckets are regularly saved to the
// backend and then marked as not dirty.
// This test provokes that webKnossos crashes (a hard crash is only done during testing; in dev/prod
// a soft warning is emitted via the devtools).
// The corresponding sibling test checks that saving inbetween does not make webKnossos crash.
t.plan(2);
t.false(hasRootSagaCrashed());
const failedSagaPromise = waitForCondition(hasRootSagaCrashed, 500);
await Promise.race([testLabelingManyBuckets(t, false), failedSagaPromise]);
t.true(hasRootSagaCrashed());
},
);

test.serial(
"Brushing/Tracing should not crash when too many buckets are labeled at once with saving inbetween",
async t => {
await t.context.api.tracing.save();

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
0,
0,
);
t.plan(2);
t.false(hasRootSagaCrashed());
const failedSagaPromise = waitForCondition(hasRootSagaCrashed, 500);
await Promise.race([testLabelingManyBuckets(t, true), failedSagaPromise]);
t.false(hasRootSagaCrashed());
},
);

async function testLabelingManyBuckets(t, saveInbetween) {
// We set MAXIMUM_BUCKET_COUNT to 150 and then label 199 = 75 (mag1) + 124 (downsampled) buckets in total.
// In between, we will save the data which allows the buckets of the first batch to be GC'ed.
// Therefore, saving the buckets of the second batch should not cause any problems.
t.context.model.getCubeByLayerName("segmentation").MAXIMUM_BUCKET_COUNT = 150;

const oldCellId = 11;
const brushSize = 10;
const newCellId = 2;

t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction(
Uint16Array,
oldCellId,
500,
);
// Reload buckets which might have already been loaded before swapping the sendJSONReceiveArraybufferWithHeaders
// function.
await t.context.api.data.reloadAllBuckets();

// Prepare to paint into the center of 50 buckets.
const paintPositions1 = _.range(0, 50).map(idx => [32 * idx + 16, 32 * idx + 16, 32 * idx + 16]);
// Prepare to paint into the center of 50 other buckets.
const paintPositions2 = _.range(50, 100).map(idx => [
32 * idx + 16,
32 * idx + 16,
32 * idx + 16,
]);

Store.dispatch(updateUserSettingAction("brushSize", brushSize));
Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH));
Store.dispatch(setActiveCellAction(newCellId));

for (const paintPosition of paintPositions1) {
Store.dispatch(setPositionAction(paintPosition));
Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY));
Store.dispatch(addToLayerAction(paintPosition));
Store.dispatch(finishEditingAction());
}

if (saveInbetween) {
await t.context.api.tracing.save();
}

for (const paintPosition of paintPositions2) {
Store.dispatch(setPositionAction(paintPosition));
Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY));
Store.dispatch(addToLayerAction(paintPosition));
Store.dispatch(finishEditingAction());
}

await t.context.api.tracing.save();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Snapshot report for `public/test-bundle/test/sagas/volumetracing_saga_integration.spec.js`
# Snapshot report for `public/test-bundle/test/sagas/volumetracing/volumetracing_saga_integration.spec.js`

The actual snapshot is saved in `volumetracing_saga_integration.spec.js.snap`.

Expand Down

0 comments on commit 102b4b0

Please sign in to comment.