github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/queryManager/saga.spec.ts (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import { assert } from "chai";
    12  import moment from "moment";
    13  
    14  import { channel } from "redux-saga";
    15  import { delay, call } from "redux-saga/effects";
    16  import { expectSaga, testSaga } from "redux-saga-test-plan";
    17  
    18  import {
    19      refresh,
    20      autoRefresh,
    21      stopAutoRefresh,
    22      ManagedQuerySagaState,
    23      processQueryManagementAction,
    24      queryManagerSaga,
    25      timeToNextRefresh,
    26      getMoment,
    27      DEFAULT_REFRESH_INTERVAL,
    28      DEFAULT_RETRY_DELAY,
    29  } from "./saga";
    30  
    31  import {
    32      queryManagerReducer,
    33  } from "./reducer";
    34  
    35  describe("Query Management Saga", function() {
    36      let queryCounterCalled = 0;
    37      const testQueryCounter = {
    38          id: "testQueryCounter",
    39          refreshInterval: moment.duration(50),
    40          retryDelay: moment.duration(500),
    41          querySaga: function* () {
    42              yield delay(0);
    43              yield call(() => queryCounterCalled++);
    44          },
    45      };
    46  
    47      const sentinelError = new Error("error");
    48      let queryErrorCalled = 0;
    49      const testQueryError = {
    50          id: "testQueryError",
    51          refreshInterval: moment.duration(500),
    52          retryDelay: moment.duration(50),
    53          querySaga: function* (): IterableIterator<void> {
    54              queryErrorCalled++;
    55              throw sentinelError;
    56          },
    57      };
    58  
    59      beforeEach(function() {
    60          queryCounterCalled = 0;
    61          queryErrorCalled = 0;
    62      });
    63  
    64      describe("integration tests", function() {
    65          describe("REFRESH action", function() {
    66              it("immediately runs a saga when refresh is called", function() {
    67                  return expectSaga(queryManagerSaga)
    68                      .dispatch(refresh(testQueryCounter))
    69                      .silentRun()
    70                      .then(() => {
    71                          assert.equal(queryCounterCalled, 1);
    72                      });
    73              });
    74              it("does not run refresh again if query is currently in progress", function() {
    75                  return expectSaga(queryManagerSaga)
    76                      .dispatch(refresh(testQueryCounter))
    77                      .dispatch(refresh(testQueryCounter))
    78                      .silentRun()
    79                      .then(() => {
    80                          assert.equal(queryCounterCalled, 1);
    81                      });
    82              });
    83              it("does refresh again if query is allowed to finish.", function() {
    84                  return expectSaga(queryManagerSaga)
    85                      .dispatch(refresh(testQueryCounter))
    86                      .delay(10)
    87                      .dispatch(refresh(testQueryCounter))
    88                      .silentRun()
    89                      .then(() => {
    90                          assert.equal(queryCounterCalled, 2);
    91                      });
    92              });
    93              it("correctly records error (and does not retry).", function() {
    94                  return expectSaga(queryManagerSaga)
    95                      .withReducer(queryManagerReducer)
    96                      .dispatch(refresh(testQueryError))
    97                      .silentRun()
    98                      .then(runResult => {
    99                          assert.isObject(runResult.storeState[testQueryError.id]);
   100                          assert.equal(runResult.storeState[testQueryError.id].lastError, sentinelError);
   101                          assert.isFalse(runResult.storeState[testQueryError.id].isRunning);
   102                      });
   103              });
   104              it("immediately runs a saga if refresh is called even if AUTO_REFRESH wait is active", function () {
   105                  return expectSaga(queryManagerSaga)
   106                      .dispatch(autoRefresh(testQueryCounter))
   107                      .delay(10)
   108                      .dispatch(refresh(testQueryCounter))
   109                      .dispatch(stopAutoRefresh(testQueryCounter))
   110                      .silentRun()
   111                      .then(() => {
   112                          assert.equal(queryCounterCalled, 2);
   113                      });
   114              });
   115          });
   116          describe("AUTO_REFRESH/STOP_AUTO_REFRESH action", function() {
   117              it("immediately runs if query result is out of date", function() {
   118                  return expectSaga(queryManagerSaga)
   119                      .dispatch(autoRefresh(testQueryCounter))
   120                      .dispatch(stopAutoRefresh(testQueryCounter))
   121                      .silentRun()
   122                      .then(() => {
   123                          assert.equal(queryCounterCalled, 1);
   124                      });
   125              });
   126              it("does not run again if query result is considered current.", function() {
   127                  return expectSaga(queryManagerSaga)
   128                      .dispatch(refresh(testQueryCounter))
   129                      .dispatch(autoRefresh(testQueryCounter))
   130                      .dispatch(stopAutoRefresh(testQueryCounter))
   131                      .silentRun()
   132                      .then(() => {
   133                          assert.equal(queryCounterCalled, 1);
   134                      });
   135              });
   136              it("runs again after a delay while refresh refcount is positive.", function() {
   137                  const tester = expectSaga(queryManagerSaga);
   138  
   139                  // A query which stops itself by dispatching a stopAutoRefresh
   140                  // after being called some number of times.
   141                  let queryCalled = 0;
   142                  const selfStopQuery = {
   143                      id: "selfStopQuery",
   144                      refreshInterval: moment.duration(50),
   145                      querySaga: function* (): IterableIterator<void> {
   146                          queryCalled++;
   147                          if (queryCalled > 3) {
   148                              tester.dispatch(stopAutoRefresh(selfStopQuery));
   149                          }
   150                      },
   151                  };
   152                  return tester
   153                      .dispatch(autoRefresh(selfStopQuery))
   154                      .dispatch(autoRefresh(selfStopQuery))
   155                      .dispatch(autoRefresh(selfStopQuery))
   156                      .silentRun(250)
   157                      .then(() => {
   158                          assert.equal(queryCalled, 5);
   159                      });
   160              });
   161              it("Uses retry delay when errors are encountered", function() {
   162                  return expectSaga(queryManagerSaga)
   163                      .dispatch(autoRefresh(testQueryError))
   164                      .silentRun(200)
   165                      .then(() => {
   166                          // RefreshTimeout is high enough that it would only be
   167                          // called once.
   168                          assert.isAtLeast(queryErrorCalled, 3);
   169                      });
   170              });
   171              it("sets inRunning flag on reducer when query is running.", function() {
   172                  const neverResolveQuery = {
   173                      id: "explicitResolveQuery",
   174                      refreshInterval: moment.duration(0),
   175                      querySaga: function* (): IterableIterator<Promise<void>> {
   176                          yield new Promise((_resolve, _reject) => {});
   177                      },
   178                  };
   179                  return expectSaga(queryManagerSaga)
   180                      .withReducer(queryManagerReducer)
   181                      .dispatch(refresh(neverResolveQuery))
   182                      .dispatch(refresh(testQueryCounter))
   183                      .silentRun()
   184                      .then(runResult => {
   185                          assert.isTrue(runResult.storeState[neverResolveQuery.id].isRunning);
   186                          assert.isFalse(runResult.storeState[testQueryCounter.id].isRunning);
   187                          assert.equal(queryCounterCalled, 1);
   188                      });
   189              });
   190              it("continues to count AUTO_REFRESH refcounts even while query is running", function() {
   191                  let queryCalledCount = 0;
   192                  let resolveQuery: () => void;
   193                  const explicitResolveQuery = {
   194                      id: "explicitResolveQuery",
   195                      refreshInterval: moment.duration(0),
   196                      querySaga: function* (): IterableIterator<Promise<void>> {
   197                          queryCalledCount++;
   198                          yield new Promise((resolve, _reject) => {
   199                              resolveQuery = resolve;
   200                          });
   201                      },
   202                  };
   203                  return async function() {
   204                      const tester = expectSaga(queryManagerSaga)
   205                          .dispatch(refresh(explicitResolveQuery));
   206  
   207                      const testFinished = tester.silentRun();
   208                      await delay(0);
   209  
   210                      // Query is now in progress, waiting on explicit resolve to
   211                      // complete. Dispatch two autoRefresh requests, which should
   212                      // still be serviced.
   213                      tester
   214                          .dispatch(autoRefresh(explicitResolveQuery))
   215                          .dispatch(autoRefresh(explicitResolveQuery));
   216  
   217                      // resolve the query, which should result in the query
   218                      // immediately being called again due to the auto-refresh
   219                      // count.
   220                      await delay(0);
   221                      resolveQuery();
   222  
   223                      // Dispatch stopAutoRefresh and resolve the query. This
   224                      // should still result in the query being called again,
   225                      // because autoRefresh has not been fully decremented.
   226                      tester
   227                          .dispatch(stopAutoRefresh(explicitResolveQuery));
   228                      await delay(0);
   229                      resolveQuery();
   230  
   231                      // Fully decrement stopAutoRefresh and resolve the query.
   232                      // Query should not be called again.
   233                      tester
   234                          .dispatch(stopAutoRefresh(explicitResolveQuery));
   235                      await delay(0);
   236                      resolveQuery();
   237                      await testFinished;
   238  
   239                      assert.equal(queryCalledCount, 3);
   240                  }();
   241              });
   242          });
   243      });
   244  
   245      describe("component unit tests", function() {
   246          describe("processQueryManagementAction", function() {
   247              it("initially processes first action", function() {
   248                  const state = new ManagedQuerySagaState();
   249                  state.channel = channel<any>();
   250                  testSaga(processQueryManagementAction, state)
   251                      .next()
   252                      .take(state.channel);
   253              });
   254              it("correctly handles REFRESH action", function() {
   255                  const state = new ManagedQuerySagaState();
   256                  state.channel = channel<any>();
   257                  testSaga(processQueryManagementAction, state)
   258                      .next()
   259                      .take(state.channel)
   260                      .next(refresh(testQueryCounter))
   261                      .isDone();
   262                  const expected = new ManagedQuerySagaState();
   263                  expected.channel = state.channel;
   264                  expected.shouldRefreshQuery = true;
   265                  assert.deepEqual(state, expected);
   266              });
   267              it("correctly handles AUTO_REFRESH action", function() {
   268                  const state = new ManagedQuerySagaState();
   269                  state.channel = channel<any>();
   270                  testSaga(processQueryManagementAction, state)
   271                      .next()
   272                      .take(state.channel)
   273                      .next(autoRefresh(testQueryCounter))
   274                      .isDone();
   275                  const expected = new ManagedQuerySagaState();
   276                  expected.channel = state.channel;
   277                  expected.autoRefreshCount = 1;
   278                  assert.equal(state.autoRefreshCount, 1);
   279                  assert.deepEqual(state, expected);
   280              });
   281              it("correctly handles STOP_AUTO_REFRESH action", function() {
   282                  const state = new ManagedQuerySagaState();
   283                  state.channel = channel<any>();
   284                  testSaga(processQueryManagementAction, state)
   285                      .next()
   286                      .take(state.channel)
   287                      .next(stopAutoRefresh(testQueryCounter))
   288                      .isDone();
   289                  const expected = new ManagedQuerySagaState();
   290                  expected.channel = state.channel;
   291                  expected.autoRefreshCount = -1;
   292                  assert.equal(state.autoRefreshCount, -1);
   293                  assert.deepEqual(state, expected);
   294              });
   295          });
   296  
   297          describe("timeToNextRefresh", function() {
   298              it("returns 0 if the query has never run.", function() {
   299                  const state = new ManagedQuerySagaState();
   300                  testSaga(timeToNextRefresh, state)
   301                      .next()
   302                      .returns(0);
   303              });
   304              it("applies refresh interval if specified.", function() {
   305                  const state = new ManagedQuerySagaState();
   306                  state.query = testQueryCounter;
   307                  state.queryCompletedAt = moment(5000);
   308                  testSaga(timeToNextRefresh, state)
   309                      .next()
   310                      .call(getMoment)
   311                      .next(5030)
   312                      .returns(testQueryCounter.refreshInterval.asMilliseconds() - 30);
   313              });
   314              it("applies default refresh interval if none specified.", function() {
   315                  const state = new ManagedQuerySagaState();
   316                  state.query = {
   317                      id: "defaultQuery",
   318                      querySaga: function* () {
   319                          return null;
   320                      },
   321                  };
   322                  state.queryCompletedAt = moment(5000);
   323                  testSaga(timeToNextRefresh, state)
   324                      .next()
   325                      .call(getMoment)
   326                      .next(5030)
   327                      .returns(DEFAULT_REFRESH_INTERVAL.asMilliseconds() - 30);
   328              });
   329              it("applies retry delay in error case if specified.", function() {
   330                  const state = new ManagedQuerySagaState();
   331                  state.query = testQueryCounter;
   332                  state.queryCompletedAt = moment(5000);
   333                  state.lastAttemptFailed = true;
   334                  testSaga(timeToNextRefresh, state)
   335                      .next()
   336                      .call(getMoment)
   337                      .next(5030)
   338                      .returns(testQueryCounter.retryDelay.asMilliseconds() - 30);
   339              });
   340              it("applies default retry delay in error case if none specified.", function() {
   341                  const state = new ManagedQuerySagaState();
   342                  state.query = {
   343                      id: "defaultQuery",
   344                      querySaga: function* () {
   345                          return null;
   346                      },
   347                  };
   348                  state.queryCompletedAt = moment(5000);
   349                  state.lastAttemptFailed = true;
   350                  testSaga(timeToNextRefresh, state)
   351                      .next()
   352                      .call(getMoment)
   353                      .next(5030)
   354                      .returns(DEFAULT_RETRY_DELAY.asMilliseconds() - 30);
   355              });
   356          });
   357      });
   358  });