Skip to content

Commit

Permalink
Join check for video readiness with check for video file existence
Browse files Browse the repository at this point in the history
  • Loading branch information
AmsterGet committed Aug 28, 2024
1 parent ff353e3 commit 3becca2
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 110 deletions.
121 changes: 55 additions & 66 deletions lib/utils/attachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,84 +23,79 @@ const fsPromises = fs.promises;
const DEFAULT_WAIT_FOR_FILE_TIMEOUT = 10000;
const DEFAULT_WAIT_FOR_FILE_INTERVAL = 500;

const base64Encode = async (filePath) => {
const bitmap = await fsPromises.readFile(filePath);
return Buffer.from(bitmap).toString('base64');
};

const getScreenshotAttachment = async (absolutePath) => {
if (!absolutePath) return absolutePath;
const name = absolutePath.split(path.sep).pop();
return {
name,
type: 'image/png',
content: await base64Encode(absolutePath),
content: await fsPromises.readFile(absolutePath, { encoding: 'base64' }),
};
};

const waitForFile = (
async function getFilePathByGlobPattern(globFilePattern) {
const files = await glob.glob(globFilePattern);

if (files.length) {
return files[0];
}

return null;
}
/*
* The moov atom in an MP4 file is a crucial part of the file’s structure. It contains metadata about the video, such as the duration, display characteristics, and timing information.
* Function check for the moov atom in file content and ensure is video file ready.
*/
const checkVideoFileReady = async (videoFilePath) => {
try {
const fileData = await fsPromises.readFile(videoFilePath);

if (fileData.includes('moov')) {
return true;
}
} catch (e) {
throw new Error(`Error reading file: ${e.message}`);
}

return false;
};

const waitForVideoFile = (
globFilePattern,
timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT,
interval = DEFAULT_WAIT_FOR_FILE_INTERVAL,
) =>
new Promise((resolve, reject) => {
let totalTime = 0;
let filePath = null;
let totalFileWaitingTime = 0;

async function checkFileExistence() {
const files = await glob(globFilePattern);
async function checkFileExistsAndReady() {
if (!filePath) {
filePath = await getFilePathByGlobPattern(globFilePattern);
}
let isVideoFileReady = false;

if (files.length) {
resolve(files[0]);
} else if (totalTime >= timeout) {
reject(new Error(`Timeout of ${timeout}ms reached, file ${globFilePattern} not found.`));
if (filePath) {
isVideoFileReady = await checkVideoFileReady(filePath);
}

if (isVideoFileReady) {
resolve(filePath);
} else if (totalFileWaitingTime >= timeout) {
reject(
new Error(
`Timeout of ${timeout}ms reached, file ${globFilePattern} not found or not ready yet.`,
),
);
} else {
totalTime += interval;
setTimeout(checkFileExistence, interval);
totalFileWaitingTime += interval;
setTimeout(checkFileExistsAndReady, interval);
}
}

checkFileExistence().catch(reject);
checkFileExistsAndReady().catch(reject);
});

const checkMoovAtom = (
mp4FilePath,
timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT,
interval = DEFAULT_WAIT_FOR_FILE_INTERVAL,
) => {
return new Promise((resolve, reject) => {
let totalTime = 0;

const checkFile = () => {
try {
fs.readFile(mp4FilePath, (err, data) => {
if (err) {
return reject(new Error(`Error reading file: ${err.message}`));
}

if (data.includes('moov')) {
return resolve(true);
}

if (totalTime >= timeout) {
return reject(
new Error(
`Timeout of ${timeout}ms reached, 'moov' atom not found in file ${mp4FilePath}.`,
),
);
}
totalTime += interval;
setTimeout(checkFile, interval);
return null;
});
} catch (error) {
return reject(new Error(`Unexpected error: ${error.message}`));
}
return null;
};
checkFile();
});
};

const getVideoFile = async (
specFileName,
videosFolder = '**',
Expand All @@ -117,14 +112,7 @@ const getVideoFile = async (
let videoFilePath;

try {
videoFilePath = await waitForFile(globFilePath, timeout, interval);
} catch (e) {
console.warn(e.message);
return null;
}

try {
await checkMoovAtom(videoFilePath, timeout, interval);
videoFilePath = await waitForVideoFile(globFilePath, timeout, interval);
} catch (e) {
console.warn(e.message);
return null;
Expand All @@ -140,6 +128,7 @@ const getVideoFile = async (
module.exports = {
getScreenshotAttachment,
getVideoFile,
waitForFile,
checkMoovAtom,
waitForVideoFile,
getFilePathByGlobPattern,
checkVideoFileReady,
};
190 changes: 146 additions & 44 deletions test/utils/attachments.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
const mock = require('mock-fs');
const fsPromises = require('fs/promises');
const mockFs = require('mock-fs');
const path = require('path');
const glob = require('glob');
const attachmentUtils = require('../../lib/utils/attachments');

const {
getScreenshotAttachment,
// getVideoFile,
waitForFile,
} = require('../../lib/utils/attachments');

jest.mock('glob');
getVideoFile,
waitForVideoFile,
getFilePathByGlobPattern,
checkVideoFileReady,
} = attachmentUtils;

const sep = path.sep;

describe('attachment utils', () => {
describe('getScreenshotAttachment', () => {
beforeEach(() => {
mock({
mockFs({
'/example/screenshots/example.spec.js': {
'suite name -- test name (failed).png': Buffer.from([8, 6, 7, 5, 3, 0, 9]),
'suite name -- test name.png': Buffer.from([1, 2, 3, 4, 5, 6, 7]),
Expand All @@ -25,7 +28,7 @@ describe('attachment utils', () => {
});

afterEach(() => {
mock.restore();
mockFs.restore();
});

it('getScreenshotAttachment: should not fail on undefined', async () => {
Expand All @@ -49,57 +52,156 @@ describe('attachment utils', () => {
});
});

describe('waitForFile', () => {
const TEST_TIMEOUT_BASED_ON_INTERVAL = 15000;
describe('getFilePathByGlobPattern', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('returns the path of the first file if files are found', async () => {
const mockFiles = ['path/to/first/file.mp4', 'path/to/second/file.mp4'];
jest.spyOn(glob, 'glob').mockResolvedValueOnce(mockFiles);

const result = await getFilePathByGlobPattern('*.mp4');
expect(result).toBe('path/to/first/file.mp4');
});

test('returns null if no files are found', async () => {
jest.spyOn(glob, 'glob').mockResolvedValueOnce([]);

const result = await getFilePathByGlobPattern('*.mp4');
expect(result).toBeNull();
});
});

describe('checkVideoFileReady', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('returns true if the video file contains "moov" atom', async () => {
const mockFileData = Buffer.from('some data with moov in it');
jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData);

const result = await checkVideoFileReady('path/to/video.mp4');
expect(result).toBe(true);
});

test('returns false if the video file does not contain "moov" atom', async () => {
const mockFileData = Buffer.from('some data without the keyword');
jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData);

const result = await checkVideoFileReady('path/to/video.mp4');
expect(result).toBe(false);
});

test('throws an error if there is an error reading the file', async () => {
jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file'));

await expect(checkVideoFileReady('path/to/video.mp4')).rejects.toThrow(
'Error reading file: Failed to read file',
);
});
});

describe('waitForVideoFile', () => {
beforeEach(() => {
jest.useFakeTimers();
glob.mockReset();
jest.clearAllMocks();
});

test('resolves with the file path if the video file is found and ready', async () => {
jest
.spyOn(attachmentUtils, 'getFilePathByGlobPattern')
.mockImplementation(async () => 'path/to/video.mp4');
// .mockResolvedValueOnce('path/to/video.mp4');
jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockImplementation(async () => true);

const promise = waitForVideoFile('*.mp4');
jest.runAllTimers();

await expect(promise).resolves.toBe('path/to/video.mp4');
}, 20000);

test('retries until the video file is ready or timeout occurs', async () => {
jest
.spyOn(attachmentUtils, 'getFilePathByGlobPattern')
.mockResolvedValueOnce('path/to/video.mp4');
jest
.spyOn(attachmentUtils, 'checkVideoFileReady')
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true);

const promise = waitForVideoFile('*.mp4');
jest.advanceTimersByTime(3000);

await expect(promise).resolves.toBe('path/to/video.mp4');
}, 20000);

test('rejects with a timeout error if the timeout is reached without finding a ready video file', async () => {
jest
.spyOn(attachmentUtils, 'getFilePathByGlobPattern')
.mockResolvedValueOnce('path/to/video.mp4');
jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockResolvedValueOnce(false);

const promise = waitForVideoFile('*.mp4', 3000, 1000);
jest.advanceTimersByTime(3000);

await expect(promise).rejects.toThrow(
'Timeout of 3000ms reached, file *.mp4 not found or not ready yet.',
);
}, 20000);

afterEach(() => {
jest.useRealTimers();
});
});

test(
'resolves when file is found immediately',
async () => {
glob.mockResolvedValue(['file1.mp4']);
describe('getVideoFile', () => {
beforeEach(() => {
jest.clearAllMocks();
});

const promise = waitForFile('*.mp4');
jest.runOnlyPendingTimers();
test('returns the correct video file object if a valid video file is found and read successfully', async () => {
const mockVideoFilePath = 'path/to/video.mp4';
const mockFileContent = 'base64encodedcontent';
jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath);
jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileContent);

await expect(promise).resolves.toBe('file1.mp4');
},
TEST_TIMEOUT_BASED_ON_INTERVAL,
);
const result = await getVideoFile('video', '**', 5000, 1000);

test(
'resolves when file is found after some intervals',
async () => {
glob
.mockResolvedValueOnce([]) // First call, no files
.mockResolvedValueOnce([]) // Second call, no files
.mockResolvedValue(['file1.mp4']); // Third call, file found
expect(result).toEqual({
name: 'video.mp4',
type: 'video/mp4',
content: mockFileContent,
});
});

const promise = waitForFile('*.mp4');
jest.advanceTimersByTime(3000);
test('returns null if no video file name is provided', async () => {
const result = await getVideoFile('');
expect(result).toBeNull();
});

await expect(promise).resolves.toBe('file1.mp4');
},
TEST_TIMEOUT_BASED_ON_INTERVAL,
);
test('returns null and logs a warning if there is an error during the video file search', async () => {
jest
.spyOn(attachmentUtils, 'waitForVideoFile')
.mockRejectedValueOnce(new Error('File not found'));
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});

test(
'rejects when timeout is reached without finding the file with custom timeout and interval',
async () => {
glob.mockResolvedValue([]);
const result = await getVideoFile('video');
expect(result).toBeNull();
expect(console.warn).toHaveBeenCalledWith('File not found');
});

const promise = waitForFile('*.mp4', 3000, 1000);
jest.advanceTimersByTime(3000);
test('handles file read errors gracefully', async () => {
const mockVideoFilePath = 'path/to/video.mp4';
jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath);
jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file'));
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});

await expect(promise).rejects.toThrow(`Timeout of 3000ms reached, file *.mp4 not found.`);
},
TEST_TIMEOUT_BASED_ON_INTERVAL,
);
const result = await getVideoFile('video');
expect(result).toBeNull();
expect(console.warn).toHaveBeenCalledWith('Failed to read file');
});
});
});

0 comments on commit 3becca2

Please sign in to comment.