addons/isl-server/src/__tests__/OperationQueue.test.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {OperationCommandProgressReporter} from 'isl/src/types';
b69ab319import type {ServerPlatform} from '../serverPlatform';
b69ab3110import type {RepositoryContext} from '../serverTypes';
b69ab3111
b69ab3112import {CommandRunner} from 'isl/src/types';
b69ab3113import {mockLogger} from 'shared/testUtils';
b69ab3114import {defer} from 'shared/utils';
b69ab3115import {OperationQueue} from '../OperationQueue';
b69ab3116import {makeServerSideTracker} from '../analytics/serverSideTracker';
b69ab3117
b69ab3118const mockTracker = makeServerSideTracker(
b69ab3119 mockLogger,
b69ab3120 {platformName: 'test'} as ServerPlatform,
b69ab3121 '0.1',
b69ab3122 jest.fn(),
b69ab3123);
b69ab3124
b69ab3125const mockCtx: RepositoryContext = {
b69ab3126 cwd: 'cwd',
b69ab3127 cmd: 'sl',
b69ab3128 logger: mockLogger,
b69ab3129 tracker: mockTracker,
b69ab3130};
b69ab3131
b69ab3132describe('OperationQueue', () => {
b69ab3133 it('runs command directly when nothing queued', async () => {
b69ab3134 const p = defer();
b69ab3135 const runCallback = jest.fn().mockImplementation(() => p.promise);
b69ab3136 const queue = new OperationQueue(runCallback);
b69ab3137
b69ab3138 const onProgress = jest.fn();
b69ab3139
b69ab3140 const runPromise = queue.runOrQueueOperation(
b69ab3141 mockCtx,
b69ab3142 {
b69ab3143 args: ['pull'],
b69ab3144 id: '1',
b69ab3145 runner: CommandRunner.Sapling,
b69ab3146 trackEventName: 'PullOperation',
b69ab3147 },
b69ab3148 onProgress,
b69ab3149 );
b69ab3150 // calls synchronously
b69ab3151 expect(runCallback).toHaveBeenCalledTimes(1);
b69ab3152
b69ab3153 p.resolve(undefined);
b69ab3154 const result = await runPromise;
b69ab3155 expect(result).toEqual('ran');
b69ab3156
b69ab3157 expect(runCallback).toHaveBeenCalledTimes(1);
b69ab3158
b69ab3159 expect(onProgress).not.toHaveBeenCalledWith(expect.objectContaining({kind: 'queue'}));
b69ab3160 });
b69ab3161
b69ab3162 it('sends spawn and info messages', async () => {
b69ab3163 const runCallback = jest
b69ab3164 .fn()
b69ab3165 .mockImplementation((_op, _cwd, prog: OperationCommandProgressReporter) => {
b69ab3166 prog('spawn');
b69ab3167 prog('stdout', 'hello');
b69ab3168 prog('stderr', 'err');
b69ab3169 prog('exit', 0);
b69ab3170
b69ab3171 return Promise.resolve(undefined);
b69ab3172 });
b69ab3173 const queue = new OperationQueue(runCallback);
b69ab3174
b69ab3175 const onProgress = jest.fn();
b69ab3176 const runPromise = queue.runOrQueueOperation(
b69ab3177 mockCtx,
b69ab3178 {
b69ab3179 args: ['pull'],
b69ab3180 id: '1',
b69ab3181 runner: CommandRunner.Sapling,
b69ab3182 trackEventName: 'PullOperation',
b69ab3183 },
b69ab3184 onProgress,
b69ab3185 );
b69ab3186
b69ab3187 const result = await runPromise;
b69ab3188 expect(result).toEqual('ran');
b69ab3189
b69ab3190 expect(onProgress).toHaveBeenCalledWith(
b69ab3191 expect.objectContaining({id: '1', kind: 'spawn', queue: []}),
b69ab3192 );
b69ab3193 expect(onProgress).toHaveBeenCalledWith(
b69ab3194 expect.objectContaining({id: '1', kind: 'stdout', message: 'hello'}),
b69ab3195 );
b69ab3196 expect(onProgress).toHaveBeenCalledWith(
b69ab3197 expect.objectContaining({id: '1', kind: 'stderr', message: 'err'}),
b69ab3198 );
b69ab3199 expect(onProgress).toHaveBeenCalledWith(
b69ab31100 expect.objectContaining({id: '1', kind: 'exit', exitCode: 0}),
b69ab31101 );
b69ab31102 });
b69ab31103
b69ab31104 it('sends abort signal', async () => {
b69ab31105 const runCallback = jest.fn().mockImplementation((_op, _cwd, prog, signal: AbortSignal) => {
b69ab31106 const p = defer();
b69ab31107 signal.addEventListener('abort', () => {
b69ab31108 p.resolve(null);
b69ab31109 prog('exit', 130);
b69ab31110 });
b69ab31111 return p;
b69ab31112 });
b69ab31113 const onProgress = jest.fn();
b69ab31114 const queue = new OperationQueue(runCallback);
b69ab31115 const id = 'abc';
b69ab31116 const op = queue.runOrQueueOperation(
b69ab31117 mockCtx,
b69ab31118 {args: [], id, runner: CommandRunner.Sapling, trackEventName: 'RunOperation'},
b69ab31119 onProgress,
b69ab31120 );
b69ab31121 queue.abortRunningOperation('wrong-id');
b69ab31122 expect(onProgress).not.toHaveBeenCalled();
b69ab31123 queue.abortRunningOperation(id);
b69ab31124 const result = await op;
b69ab31125 expect(result).toEqual('ran');
b69ab31126
b69ab31127 expect(onProgress).toHaveBeenCalledWith(
b69ab31128 expect.objectContaining({id, kind: 'exit', exitCode: 130}),
b69ab31129 );
b69ab31130 });
b69ab31131
b69ab31132 it('queues up commands', async () => {
b69ab31133 const p1 = defer();
b69ab31134 const p2 = defer();
b69ab31135 const runP1 = jest.fn(() => p1.promise);
b69ab31136 const runP2 = jest.fn(() => p2.promise);
b69ab31137 const runCallback = jest.fn().mockImplementationOnce(runP1).mockImplementationOnce(runP2);
b69ab31138 const queue = new OperationQueue(runCallback);
b69ab31139
b69ab31140 const onProgress = jest.fn();
b69ab31141 expect(runP1).not.toHaveBeenCalled();
b69ab31142 expect(runP2).not.toHaveBeenCalled();
b69ab31143
b69ab31144 const runPromise1 = queue.runOrQueueOperation(
b69ab31145 mockCtx,
b69ab31146 {
b69ab31147 args: ['pull'],
b69ab31148 id: '1',
b69ab31149 runner: CommandRunner.Sapling,
b69ab31150 trackEventName: 'PullOperation',
b69ab31151 },
b69ab31152 onProgress,
b69ab31153 );
b69ab31154 expect(runP1).toHaveBeenCalled();
b69ab31155 expect(runP2).not.toHaveBeenCalled();
b69ab31156
b69ab31157 const runPromise2 = queue.runOrQueueOperation(
b69ab31158 mockCtx,
b69ab31159 {
b69ab31160 args: ['rebase'],
b69ab31161 id: '2',
b69ab31162 runner: CommandRunner.Sapling,
b69ab31163 trackEventName: 'RebaseOperation',
b69ab31164 },
b69ab31165 onProgress,
b69ab31166 );
b69ab31167 expect(runP1).toHaveBeenCalled();
b69ab31168 expect(runP2).not.toHaveBeenCalled(); // it's queued up
b69ab31169 // should notify that the command queued when it is attempted to be run
b69ab31170 expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({kind: 'queue', queue: ['2']}));
b69ab31171
b69ab31172 p1.resolve(undefined);
b69ab31173 const result1 = await runPromise1;
b69ab31174 expect(result1).toEqual('ran');
b69ab31175
b69ab31176 // now it's dequeued
b69ab31177 expect(runP2).toHaveBeenCalled();
b69ab31178
b69ab31179 p2.resolve(undefined);
b69ab31180 const result2 = await runPromise2;
b69ab31181 expect(result2).toEqual('ran');
b69ab31182
b69ab31183 expect(runCallback).toHaveBeenCalledTimes(2);
b69ab31184 });
b69ab31185
b69ab31186 it('clears queue when an operation fails', async () => {
b69ab31187 const p1 = defer();
b69ab31188 const p2 = defer();
b69ab31189 const runP1 = jest.fn(() => p1.promise);
b69ab31190 const runP2 = jest.fn(() => p2.promise);
b69ab31191 const runCallback = jest.fn().mockImplementationOnce(runP1).mockImplementationOnce(runP2);
b69ab31192 const queue = new OperationQueue(runCallback);
b69ab31193
b69ab31194 const onProgress = jest.fn();
b69ab31195 expect(runP1).not.toHaveBeenCalled();
b69ab31196 expect(runP2).not.toHaveBeenCalled();
b69ab31197
b69ab31198 const runPromise1 = queue.runOrQueueOperation(
b69ab31199 mockCtx,
b69ab31200 {
b69ab31201 args: ['pull'],
b69ab31202 id: '1',
b69ab31203 runner: CommandRunner.Sapling,
b69ab31204 trackEventName: 'PullOperation',
b69ab31205 },
b69ab31206 onProgress,
b69ab31207 );
b69ab31208 expect(runP1).toHaveBeenCalled();
b69ab31209 expect(runP2).not.toHaveBeenCalled();
b69ab31210 const runPromise2 = queue.runOrQueueOperation(
b69ab31211 mockCtx,
b69ab31212 {
b69ab31213 args: ['rebase'],
b69ab31214 id: '2',
b69ab31215 runner: CommandRunner.Sapling,
b69ab31216 trackEventName: 'RebaseOperation',
b69ab31217 },
b69ab31218 onProgress,
b69ab31219 );
b69ab31220 expect(runP1).toHaveBeenCalled();
b69ab31221 expect(runP2).not.toHaveBeenCalled(); // it's queued up
b69ab31222
b69ab31223 p1.reject(new Error('fake error'));
b69ab31224 // run promise still resolves, but error message was sent
b69ab31225 const result1 = await runPromise1;
b69ab31226 expect(result1).toEqual('ran');
b69ab31227 expect(onProgress).toHaveBeenCalledWith(
b69ab31228 expect.objectContaining({id: '1', kind: 'error', error: 'Error: fake error'}),
b69ab31229 );
b69ab31230
b69ab31231 // p2 was cancelled by p1 failing
b69ab31232 expect(runP2).not.toHaveBeenCalled();
b69ab31233 const result2 = await runPromise2;
b69ab31234 expect(result2).toEqual('skipped');
b69ab31235 expect(runCallback).toHaveBeenCalledTimes(1);
b69ab31236 });
b69ab31237
b69ab31238 it('can run commands again after an error', async () => {
b69ab31239 const p1 = defer();
b69ab31240 const p2 = defer();
b69ab31241 const runP1 = jest.fn(() => p1.promise);
b69ab31242 const runP2 = jest.fn(() => p2.promise);
b69ab31243 const runCallback = jest.fn().mockImplementationOnce(runP1).mockImplementationOnce(runP2);
b69ab31244 const queue = new OperationQueue(runCallback);
b69ab31245
b69ab31246 const onProgress = jest.fn();
b69ab31247 expect(runP1).not.toHaveBeenCalled();
b69ab31248 expect(runP2).not.toHaveBeenCalled();
b69ab31249
b69ab31250 const runPromise1 = queue.runOrQueueOperation(
b69ab31251 mockCtx,
b69ab31252 {
b69ab31253 args: ['pull'],
b69ab31254 id: '1',
b69ab31255 runner: CommandRunner.Sapling,
b69ab31256 trackEventName: 'PullOperation',
b69ab31257 },
b69ab31258 onProgress,
b69ab31259 );
b69ab31260
b69ab31261 p1.reject(new Error('fake error'));
b69ab31262 await runPromise1;
b69ab31263
b69ab31264 // after p1 errors, run another operation
b69ab31265 const runPromise2 = queue.runOrQueueOperation(
b69ab31266 mockCtx,
b69ab31267 {
b69ab31268 args: ['rebase'],
b69ab31269 id: '2',
b69ab31270 runner: CommandRunner.Sapling,
b69ab31271 trackEventName: 'RebaseOperation',
b69ab31272 },
b69ab31273 onProgress,
b69ab31274 );
b69ab31275
b69ab31276 // p2 runs immediately
b69ab31277 p2.resolve(undefined);
b69ab31278 await runPromise2;
b69ab31279 expect(runP2).toHaveBeenCalled();
b69ab31280 expect(runCallback).toHaveBeenCalledTimes(2);
b69ab31281 });
b69ab31282});