From 3becca2d793772d4075f3a038dcf49465fda9ab5 Mon Sep 17 00:00:00 2001 From: Ilya Hancharyk Date: Wed, 28 Aug 2024 18:24:24 +0200 Subject: [PATCH] Join check for video readiness with check for video file existence --- lib/utils/attachments.js | 121 ++++++++++----------- test/utils/attachments.test.js | 190 +++++++++++++++++++++++++-------- 2 files changed, 201 insertions(+), 110 deletions(-) diff --git a/lib/utils/attachments.js b/lib/utils/attachments.js index 672bf16..e68afbf 100644 --- a/lib/utils/attachments.js +++ b/lib/utils/attachments.js @@ -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 = '**', @@ -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; @@ -140,6 +128,7 @@ const getVideoFile = async ( module.exports = { getScreenshotAttachment, getVideoFile, - waitForFile, - checkMoovAtom, + waitForVideoFile, + getFilePathByGlobPattern, + checkVideoFileReady, }; diff --git a/test/utils/attachments.test.js b/test/utils/attachments.test.js index af70850..bccc9c0 100644 --- a/test/utils/attachments.test.js +++ b/test/utils/attachments.test.js @@ -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]), @@ -25,7 +28,7 @@ describe('attachment utils', () => { }); afterEach(() => { - mock.restore(); + mockFs.restore(); }); it('getScreenshotAttachment: should not fail on undefined', async () => { @@ -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'); + }); }); });