generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
When linting a project, we need to constantly read the files within either the module template or the project. We want this tool to run as fast as possible, and to do this, we can cache the metadata and content of each file that we read so that we don't have to do it again. This class assists with that. --- Co-authored-by: Mark Stacey <markjstacey@gmail.com>
- Loading branch information
Showing
8 changed files
with
571 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { writeFile } from '@metamask/utils/node'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
|
||
import { getEntryStats } from './misc-utils'; | ||
import { withinSandbox } from '../tests/helpers'; | ||
|
||
describe('getEntryStats', () => { | ||
describe('given a file', () => { | ||
it('returns the stats for the file', async () => { | ||
expect.assertions(3); | ||
|
||
await withinSandbox(async (sandbox) => { | ||
const filePath = path.join(sandbox.directoryPath, 'nonexistent.file'); | ||
await writeFile(filePath, ''); | ||
|
||
const stats = await getEntryStats(filePath); | ||
|
||
expect(stats).toHaveProperty('atime'); | ||
expect(stats).toHaveProperty('ctime'); | ||
expect(stats).toHaveProperty('mtime'); | ||
}); | ||
}); | ||
|
||
it('returns null if the file does not exist', async () => { | ||
expect.assertions(1); | ||
|
||
await withinSandbox(async (sandbox) => { | ||
const filePath = path.join(sandbox.directoryPath, 'nonexistent.file'); | ||
|
||
expect(await getEntryStats(filePath)).toBeNull(); | ||
}); | ||
}); | ||
|
||
it('re-throws a wrapped version of any other error that occurs, assigning it the same code and giving it a stack', async () => { | ||
expect.assertions(1); | ||
|
||
await withinSandbox(async (sandbox) => { | ||
const filePath = path.join(sandbox.directoryPath, 'nonexistent.file'); | ||
await writeFile(filePath, ''); | ||
try { | ||
// Make sandbox root directory non-executable. | ||
await fs.promises.chmod(sandbox.directoryPath, 0o600); | ||
|
||
await expect(getEntryStats(filePath)).rejects.toThrow( | ||
expect.objectContaining({ | ||
message: `Could not get stats for file or directory '${filePath}'`, | ||
code: 'EACCES', | ||
stack: expect.any(String), | ||
cause: expect.objectContaining({ | ||
message: `EACCES: permission denied, stat '${filePath}'`, | ||
code: 'EACCES', | ||
}), | ||
}), | ||
); | ||
} finally { | ||
// Make sandbox root directory executable again. | ||
// Ideally, this should be handled by @metamask/utils. | ||
await fs.promises.chmod(sandbox.directoryPath, 0o700); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { isErrorWithCode, wrapError } from '@metamask/utils/node'; | ||
import type fs from 'fs'; | ||
import * as fsPromises from 'fs/promises'; | ||
|
||
/** | ||
* Retrieves information about the file or directory using `fs.stat`. | ||
* | ||
* @param entryPath - The path to the file or directory. | ||
* @returns The stats for the file or directory if it exists, or null if it does | ||
* not exist. | ||
* @throws An error with a stack trace if reading fails in any way. | ||
*/ | ||
export async function getEntryStats( | ||
entryPath: string, | ||
): Promise<fs.Stats | null> { | ||
try { | ||
return await fsPromises.stat(entryPath); | ||
} catch (error) { | ||
if (isErrorWithCode(error) && error.code === 'ENOENT') { | ||
return null; | ||
} | ||
throw wrapError( | ||
error, | ||
`Could not get stats for file or directory '${entryPath}'`, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
import * as utils from '@metamask/utils/node'; | ||
import { | ||
ensureDirectoryStructureExists, | ||
writeFile, | ||
} from '@metamask/utils/node'; | ||
import fs from 'fs'; | ||
import { mock } from 'jest-mock-extended'; | ||
import path from 'path'; | ||
|
||
import { RepositoryFilesystem } from './repository-filesystem'; | ||
import { withinSandbox } from '../tests/helpers'; | ||
|
||
jest.mock('@metamask/utils/node', () => { | ||
return { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
__esModule: true, | ||
...jest.requireActual('@metamask/utils/node'), | ||
}; | ||
}); | ||
|
||
const utilsMock = jest.mocked(utils); | ||
|
||
describe('RepositoryFilesystem', () => { | ||
describe('readFile', () => { | ||
describe('if the file has not already been read', () => { | ||
it('reads the file from the repository directory', async () => { | ||
jest.spyOn(utilsMock, 'readFile').mockResolvedValue('some content'); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
|
||
await repositoryFilesystem.readFile('some.file'); | ||
|
||
expect(utilsMock.readFile).toHaveBeenCalledWith( | ||
'/some/directory/some.file', | ||
); | ||
}); | ||
|
||
it('returns the content of the file, with extra whitespace trimmed', async () => { | ||
await withinSandbox(async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await writeFile( | ||
path.join(sandboxDirectoryPath, 'some.file'), | ||
' some content ', | ||
); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
|
||
const content = await repositoryFilesystem.readFile('some.file'); | ||
|
||
expect(content).toBe('some content'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('if the file has already been read', () => { | ||
it('does not read the file from the repository directory again', async () => { | ||
jest.spyOn(utilsMock, 'readFile').mockResolvedValue('some content'); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
await repositoryFilesystem.readFile('/some/file'); | ||
|
||
await repositoryFilesystem.readFile('/some/file'); | ||
|
||
expect(utilsMock.readFile).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('returns the content of the file, with extra whitespace trimmed', async () => { | ||
await withinSandbox(async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await writeFile( | ||
path.join(sandboxDirectoryPath, 'some.file'), | ||
' some content ', | ||
); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
await repositoryFilesystem.readFile('some.file'); | ||
|
||
const content = await repositoryFilesystem.readFile('some.file'); | ||
|
||
expect(content).toBe('some content'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getEntryStats', () => { | ||
describe('given a file', () => { | ||
describe('if stats have not been requested for the file already', () => { | ||
it('requests the stats for the file', async () => { | ||
jest.spyOn(fs.promises, 'stat').mockResolvedValue(mock<fs.Stats>()); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
|
||
await repositoryFilesystem.getEntryStats('some-entry'); | ||
|
||
expect(fs.promises.stat).toHaveBeenCalledWith( | ||
'/some/directory/some-entry', | ||
); | ||
}); | ||
|
||
it('returns stats for the file', async () => { | ||
await withinSandbox( | ||
async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await writeFile(path.join(sandboxDirectoryPath, 'some.file'), ''); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
|
||
const stats = await repositoryFilesystem.getEntryStats( | ||
'some.file', | ||
); | ||
|
||
expect(stats).toHaveProperty('atime'); | ||
expect(stats).toHaveProperty('ctime'); | ||
expect(stats).toHaveProperty('mtime'); | ||
}, | ||
); | ||
}); | ||
}); | ||
|
||
describe('if stats have been requested for the file already', () => { | ||
it('does not request the stats for the file again', async () => { | ||
jest.spyOn(fs.promises, 'stat').mockResolvedValue(mock<fs.Stats>()); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
await repositoryFilesystem.getEntryStats('some-entry'); | ||
|
||
await repositoryFilesystem.getEntryStats('some-entry'); | ||
|
||
expect(fs.promises.stat).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('returns stats for the file', async () => { | ||
await withinSandbox( | ||
async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await writeFile(path.join(sandboxDirectoryPath, 'some.file'), ''); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
await repositoryFilesystem.getEntryStats('some.file'); | ||
|
||
const stats = await repositoryFilesystem.getEntryStats( | ||
'some.file', | ||
); | ||
|
||
expect(stats).toHaveProperty('atime'); | ||
expect(stats).toHaveProperty('ctime'); | ||
expect(stats).toHaveProperty('mtime'); | ||
}, | ||
); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('given a directory', () => { | ||
describe('if stats have not been requested for the directory already', () => { | ||
it('requests the stats for the directory', async () => { | ||
jest.spyOn(fs.promises, 'stat').mockResolvedValue(mock<fs.Stats>()); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
|
||
await repositoryFilesystem.getEntryStats('/another/directory'); | ||
|
||
expect(fs.promises.stat).toHaveBeenCalledWith( | ||
'/some/directory/another/directory', | ||
); | ||
}); | ||
|
||
it('returns stats for the directory', async () => { | ||
await withinSandbox( | ||
async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await ensureDirectoryStructureExists( | ||
path.join(sandboxDirectoryPath, 'some-directory'), | ||
); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
|
||
const stats = await repositoryFilesystem.getEntryStats( | ||
'some-directory', | ||
); | ||
|
||
expect(stats).toHaveProperty('atime'); | ||
expect(stats).toHaveProperty('ctime'); | ||
expect(stats).toHaveProperty('mtime'); | ||
}, | ||
); | ||
}); | ||
}); | ||
|
||
describe('if stats have been requested for the directory already', () => { | ||
it('does not request the stats for the directory again', async () => { | ||
jest.spyOn(fs.promises, 'stat').mockResolvedValue(mock<fs.Stats>()); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
'/some/directory', | ||
); | ||
await repositoryFilesystem.getEntryStats('another-directory'); | ||
|
||
await repositoryFilesystem.getEntryStats('another-directory'); | ||
|
||
expect(fs.promises.stat).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('returns stats for the directory', async () => { | ||
await withinSandbox( | ||
async ({ directoryPath: sandboxDirectoryPath }) => { | ||
await ensureDirectoryStructureExists( | ||
path.join(sandboxDirectoryPath, 'some-directory'), | ||
); | ||
const repositoryFilesystem = new RepositoryFilesystem( | ||
sandboxDirectoryPath, | ||
); | ||
await repositoryFilesystem.getEntryStats('some-directory'); | ||
|
||
const stats = await repositoryFilesystem.getEntryStats( | ||
'some-directory', | ||
); | ||
|
||
expect(stats).toHaveProperty('atime'); | ||
expect(stats).toHaveProperty('ctime'); | ||
expect(stats).toHaveProperty('mtime'); | ||
}, | ||
); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.