Files
JSQuestions/solutions/async-scheduler.test.js
T
2026-02-07 10:05:57 +05:30

271 lines
8.6 KiB
JavaScript

const TaskScheduler = require('./async-scheduler');
describe('TaskScheduler', () => {
describe('constructor', () => {
it('should create a scheduler with custom concurrency', () => {
const scheduler = new TaskScheduler(5);
expect(scheduler.concurrencyLimit).toBe(5);
});
it('should start in paused mode by default', () => {
const scheduler = new TaskScheduler();
expect(scheduler.isProcessing).toBe(false);
});
});
describe('addTask', () => {
it('should return a promise', () => {
const scheduler = new TaskScheduler();
const promise = scheduler.addTask(() => Promise.resolve('result'));
expect(promise).toBeInstanceOf(Promise);
});
it('should resolve when task completes successfully', async () => {
const scheduler = new TaskScheduler();
scheduler.play();
const promise = scheduler.addTask(() => Promise.resolve('success'));
await expect(promise).resolves.toBe('success');
});
it('should reject when task fails', async () => {
const scheduler = new TaskScheduler();
scheduler.play();
const promise = scheduler.addTask(() => Promise.reject(new Error('task failed')));
promise.catch(() => {}); // Prevent unhandled rejection
await expect(promise).rejects.toThrow('task failed');
});
it('should capture synchronous throws as rejections', async () => {
const scheduler = new TaskScheduler();
scheduler.play();
const promise = scheduler.addTask(() => {
throw new Error('sync error');
});
promise.catch(() => {}); // Prevent unhandled rejection
await expect(promise).rejects.toThrow('sync error');
});
it('should queue task if scheduler is paused', async () => {
const scheduler = new TaskScheduler();
const mockFn = jest.fn(() => Promise.resolve());
scheduler.addTask(mockFn);
await new Promise(r => setTimeout(r, 50));
expect(mockFn).not.toHaveBeenCalled();
});
});
describe('play and pause', () => {
it('should start processing when play() is called', async () => {
const scheduler = new TaskScheduler();
const mockFn = jest.fn(() => Promise.resolve());
scheduler.addTask(mockFn);
expect(mockFn).not.toHaveBeenCalled();
scheduler.play();
await new Promise(r => setTimeout(r, 10));
expect(mockFn).toHaveBeenCalled();
});
it('should stop starting new tasks when pause() is called', async () => {
const scheduler = new TaskScheduler(1);
const mockFn1 = jest.fn(() => new Promise(r => setTimeout(r, 50)));
const mockFn2 = jest.fn(() => Promise.resolve());
scheduler.play();
scheduler.addTask(mockFn1);
scheduler.addTask(mockFn2);
scheduler.pause();
await new Promise(r => setTimeout(r, 100));
expect(mockFn2).not.toHaveBeenCalled();
});
it('should resume processing when play() is called again', async () => {
const scheduler = new TaskScheduler(1);
const mockFn1 = jest.fn(() => new Promise(r => setTimeout(() => r(), 50)));
const mockFn2 = jest.fn(() => Promise.resolve());
scheduler.play();
scheduler.addTask(mockFn1);
scheduler.pause();
scheduler.addTask(mockFn2);
await new Promise(r => setTimeout(r, 60));
expect(mockFn2).not.toHaveBeenCalled();
scheduler.play();
await new Promise(r => setTimeout(r, 60));
expect(mockFn2).toHaveBeenCalled();
});
});
describe('concurrency', () => {
it('should execute up to concurrency limit simultaneously', async () => {
const scheduler = new TaskScheduler(2);
const executionTimes = [];
const createTask = (id) => async () => {
executionTimes.push({ id, event: "start" });
await new Promise(r => setTimeout(r, 50));
executionTimes.push({ id, event: "end" });
};
scheduler.play();
await Promise.all([
scheduler.addTask(createTask(1)),
scheduler.addTask(createTask(2)),
scheduler.addTask(createTask(3)),
]);
// Expected order:
// 1. Task 1 starts
// 2. Task 2 starts (parallel, within limit)
// 3. Task 1 or 2 ends
// 4. Task 3 starts (frees up a slot)
// 5. Task 3 ends
expect(executionTimes[0]).toEqual({id: 1, event: "start"});
expect(executionTimes[1]).toEqual({id: 2, event: "start"});
expect(executionTimes[2]).toEqual({id: 1, event: "end"});
expect(executionTimes[3]).toEqual({id: 3, event: "start"});
expect(executionTimes[4]).toEqual({id: 2, event: "end"});
expect(executionTimes[5]).toEqual({id: 3, event: "end"});
});
it('should queue tasks when concurrency limit is reached', async () => {
const scheduler = new TaskScheduler(1);
const execOrder = [];
const createTask = (id) => async () => {
execOrder.push(`start-${id}`);
await new Promise(r => setTimeout(r, 10));
execOrder.push(`end-${id}`);
};
scheduler.play();
await Promise.all([
scheduler.addTask(createTask(1)),
scheduler.addTask(createTask(2)),
scheduler.addTask(createTask(3)),
]);
expect(execOrder).toEqual([
'start-1', 'end-1',
'start-2', 'end-2',
'start-3', 'end-3',
]);
});
});
describe('FIFO ordering', () => {
it('should execute tasks in FIFO order', async () => {
const scheduler = new TaskScheduler(1);
const execOrder = [];
const createTask = (id) => () => {
execOrder.push(id);
return Promise.resolve();
};
scheduler.play();
await Promise.all([
scheduler.addTask(createTask(1)),
scheduler.addTask(createTask(2)),
scheduler.addTask(createTask(3)),
scheduler.addTask(createTask(4)),
]);
expect(execOrder).toEqual([1, 2, 3, 4]);
});
it('should maintain FIFO order even with variable task durations', async () => {
const scheduler = new TaskScheduler(1);
const execOrder = [];
const createTask = (id, duration) => async () => {
execOrder.push(`start-${id}`);
await new Promise(r => setTimeout(r, duration));
execOrder.push(`end-${id}`);
};
scheduler.play();
await Promise.all([
scheduler.addTask(createTask(1, 30)),
scheduler.addTask(createTask(2, 10)),
scheduler.addTask(createTask(3, 20)),
]);
expect(execOrder).toEqual([
'start-1', 'end-1',
'start-2', 'end-2',
'start-3', 'end-3',
]);
});
});
describe('automatic task pickup after completion', () => {
it('should pick next task from queue when running task completes', async () => {
const scheduler = new TaskScheduler(1);
const mockFn2 = jest.fn(() => Promise.resolve());
scheduler.play();
await scheduler.addTask(() => new Promise(r => setTimeout(r, 20)));
const p2 = scheduler.addTask(mockFn2);
await p2;
expect(mockFn2).toHaveBeenCalled();
});
it('should pick next task even if current task rejects', async () => {
const scheduler = new TaskScheduler(1);
const mockFn2 = jest.fn(() => Promise.resolve());
scheduler.play();
const p1 = scheduler.addTask(() => Promise.reject(new Error("this should fail")));
p1.catch(() => {}); // suppress unhandled rejection
const p2 = scheduler.addTask(mockFn2);
await p2;
expect(mockFn2).toHaveBeenCalled();
});
});
describe('edge cases', () => {
it('should handle concurrency of 1', async () => {
const scheduler = new TaskScheduler(1);
const execOrder = [];
scheduler.play();
await Promise.all([
scheduler.addTask(() => { execOrder.push(1); return Promise.resolve(); }),
scheduler.addTask(() => { execOrder.push(2); return Promise.resolve(); }),
]);
expect(execOrder).toEqual([1, 2]);
});
it('should handle large concurrency', async () => {
const scheduler = new TaskScheduler(100);
const promises = [];
scheduler.play();
for (let i = 0; i < 50; i++) {
promises.push(scheduler.addTask(() => Promise.resolve(i)));
}
const results = await Promise.all(promises);
expect(results.length).toBe(50);
});
it('should handle empty queue gracefully', () => {
const scheduler = new TaskScheduler();
scheduler.play();
// Should not throw
expect(() => scheduler._processQueue()).not.toThrow();
});
});
});