329 lines
9.2 KiB
TypeScript
329 lines
9.2 KiB
TypeScript
import { expect, describe, test, beforeEach, afterEach } from 'bun:test'
|
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'
|
|
import { join, resolve } from 'path'
|
|
import { fs } from '../fs'
|
|
|
|
const TEST_DIR = resolve('./tmp/shrimp-fs-test')
|
|
const CWD = process.cwd()
|
|
|
|
beforeEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true })
|
|
}
|
|
mkdirSync(TEST_DIR, { recursive: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.chdir(CWD)
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true })
|
|
}
|
|
})
|
|
|
|
describe('fs - directory operations', () => {
|
|
test('fs.ls lists directory contents', () => {
|
|
writeFileSync(join(TEST_DIR, 'file1.txt'), 'content1')
|
|
writeFileSync(join(TEST_DIR, 'file2.txt'), 'content2')
|
|
|
|
const result = fs.ls(TEST_DIR)
|
|
expect(result).toContain('file1.txt')
|
|
expect(result).toContain('file2.txt')
|
|
})
|
|
|
|
test('fs.mkdir creates directory', () => {
|
|
const newDir = join(TEST_DIR, 'newdir')
|
|
fs.mkdir(newDir)
|
|
expect(existsSync(newDir)).toBe(true)
|
|
})
|
|
|
|
test('fs.rmdir removes empty directory', () => {
|
|
const dir = join(TEST_DIR, 'toremove')
|
|
mkdirSync(dir)
|
|
fs.rmdir(dir)
|
|
expect(existsSync(dir)).toBe(false)
|
|
})
|
|
|
|
test('fs.pwd returns current working directory', () => {
|
|
const result = fs.pwd()
|
|
expect(typeof result).toBe('string')
|
|
expect(result.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('fs.cd changes current working directory', () => {
|
|
const originalCwd = process.cwd()
|
|
fs.cd(TEST_DIR)
|
|
expect(process.cwd()).toBe(TEST_DIR)
|
|
process.chdir(originalCwd) // restore
|
|
})
|
|
})
|
|
|
|
describe('fs - reading', () => {
|
|
test('fs.read reads file contents as string', () => {
|
|
const file = join(TEST_DIR, 'test.txt')
|
|
writeFileSync(file, 'hello world')
|
|
|
|
const result = fs.read(file)
|
|
expect(result).toBe('hello world')
|
|
})
|
|
|
|
test('fs.cat is alias for fs.read', () => {
|
|
const file = join(TEST_DIR, 'test.txt')
|
|
writeFileSync(file, 'hello world')
|
|
|
|
const result = fs.cat(file)
|
|
expect(result).toBe('hello world')
|
|
})
|
|
|
|
test('fs.read-bytes reads file as buffer', () => {
|
|
const file = join(TEST_DIR, 'test.bin')
|
|
writeFileSync(file, Buffer.from([1, 2, 3, 4]))
|
|
|
|
const result = fs['read-bytes'](file)
|
|
expect(result).toBeInstanceOf(Array)
|
|
expect(result).toEqual([1, 2, 3, 4])
|
|
})
|
|
})
|
|
|
|
describe('fs - writing', () => {
|
|
test('fs.write writes string to file', async () => {
|
|
const file = join(TEST_DIR, 'output.txt')
|
|
fs.write(file, 'test content')
|
|
|
|
const content = Bun.file(file).text()
|
|
expect(await content).toBe('test content')
|
|
})
|
|
|
|
test('fs.append appends to existing file', async () => {
|
|
const file = join(TEST_DIR, 'append.txt')
|
|
writeFileSync(file, 'first')
|
|
fs.append(file, ' second')
|
|
|
|
const content = await Bun.file(file).text()
|
|
expect(content).toBe('first second')
|
|
})
|
|
})
|
|
|
|
describe('fs - file operations', () => {
|
|
test('fs.rm removes file', () => {
|
|
const file = join(TEST_DIR, 'remove.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
fs.rm(file)
|
|
expect(existsSync(file)).toBe(false)
|
|
})
|
|
|
|
test('fs.delete is alias for fs.rm', () => {
|
|
const file = join(TEST_DIR, 'delete.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
fs.delete(file)
|
|
expect(existsSync(file)).toBe(false)
|
|
})
|
|
|
|
test('fs.copy copies file', async () => {
|
|
const src = join(TEST_DIR, 'source.txt')
|
|
const dest = join(TEST_DIR, 'dest.txt')
|
|
writeFileSync(src, 'content')
|
|
|
|
fs.copy(src, dest)
|
|
expect(await Bun.file(dest).text()).toBe('content')
|
|
})
|
|
|
|
test('fs.cp is alias for fs.copy', async () => {
|
|
const src = join(TEST_DIR, 'source2.txt')
|
|
const dest = join(TEST_DIR, 'dest2.txt')
|
|
writeFileSync(src, 'content')
|
|
|
|
fs.cp(src, dest)
|
|
expect(await Bun.file(dest).text()).toBe('content')
|
|
})
|
|
|
|
test('fs.move moves file', async () => {
|
|
const src = join(TEST_DIR, 'source.txt')
|
|
const dest = join(TEST_DIR, 'moved.txt')
|
|
writeFileSync(src, 'content')
|
|
|
|
fs.move(src, dest)
|
|
expect(existsSync(src)).toBe(false)
|
|
expect(await Bun.file(dest).text()).toBe('content')
|
|
})
|
|
|
|
test('fs.mv is alias for fs.move', async () => {
|
|
const src = join(TEST_DIR, 'source2.txt')
|
|
const dest = join(TEST_DIR, 'moved2.txt')
|
|
writeFileSync(src, 'content')
|
|
|
|
fs.mv(src, dest)
|
|
expect(existsSync(src)).toBe(false)
|
|
expect(await Bun.file(dest).text()).toBe('content')
|
|
})
|
|
})
|
|
|
|
describe('fs - path operations', () => {
|
|
test('fs.basename extracts filename from path', () => {
|
|
expect(fs.basename('/path/to/file.txt')).toBe('file.txt')
|
|
expect(fs.basename('/path/to/dir/')).toBe('dir')
|
|
})
|
|
|
|
test('fs.dirname extracts directory from path', () => {
|
|
expect(fs.dirname('/path/to/file.txt')).toBe('/path/to')
|
|
expect(fs.dirname('/path/to/dir/')).toBe('/path/to')
|
|
})
|
|
|
|
test('fs.extname extracts file extension', () => {
|
|
expect(fs.extname('file.txt')).toBe('.txt')
|
|
expect(fs.extname('file.tar.gz')).toBe('.gz')
|
|
expect(fs.extname('noext')).toBe('')
|
|
})
|
|
|
|
test('fs.join joins path segments', () => {
|
|
expect(fs.join('path', 'to', 'file.txt')).toBe('path/to/file.txt')
|
|
expect(fs.join('/absolute', 'path')).toBe('/absolute/path')
|
|
})
|
|
|
|
test('fs.resolve resolves to absolute path', () => {
|
|
const result = fs.resolve('relative', 'path')
|
|
expect(result.startsWith('/')).toBe(true)
|
|
expect(result).toContain('relative')
|
|
})
|
|
})
|
|
|
|
describe('fs - file info', () => {
|
|
test('fs.stat returns file stats', () => {
|
|
const file = join(TEST_DIR, 'stat.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
const stats = fs.stat(file)
|
|
expect(stats).toHaveProperty('size')
|
|
expect(stats).toHaveProperty('mtime')
|
|
expect(stats.size).toBe(7) // 'content' is 7 bytes
|
|
})
|
|
|
|
test('fs.exists? checks if path exists', () => {
|
|
const file = join(TEST_DIR, 'exists.txt')
|
|
expect(fs['exists?'](file)).toBe(false)
|
|
|
|
writeFileSync(file, 'content')
|
|
expect(fs['exists?'](file)).toBe(true)
|
|
})
|
|
|
|
test('fs.file? checks if path is a file', () => {
|
|
const file = join(TEST_DIR, 'isfile.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
expect(fs['file?'](file)).toBe(true)
|
|
expect(fs['file?'](TEST_DIR)).toBe(false)
|
|
})
|
|
|
|
test('fs.dir? checks if path is a directory', () => {
|
|
const dir = join(TEST_DIR, 'isdir')
|
|
mkdirSync(dir)
|
|
|
|
expect(fs['dir?'](dir)).toBe(true)
|
|
expect(fs['dir?'](join(TEST_DIR, 'isfile.txt'))).toBe(false)
|
|
})
|
|
|
|
test('fs.symlink? checks if path is a symbolic link', () => {
|
|
const file = join(TEST_DIR, 'target.txt')
|
|
const link = join(TEST_DIR, 'link.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
fs.symlink(file, link)
|
|
expect(fs['symlink?'](link)).toBe(true)
|
|
expect(fs['symlink?'](file)).toBe(false)
|
|
})
|
|
|
|
test('fs.exec? checks if file is executable', () => {
|
|
const file = join(TEST_DIR, 'script.sh')
|
|
writeFileSync(file, '#!/bin/bash\necho hello')
|
|
|
|
fs.chmod(file, 0o755)
|
|
expect(fs['exec?'](file)).toBe(true)
|
|
|
|
fs.chmod(file, 0o644)
|
|
expect(fs['exec?'](file)).toBe(false)
|
|
})
|
|
|
|
test('fs.size returns file size in bytes', () => {
|
|
const file = join(TEST_DIR, 'sizeme.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
expect(fs.size(file)).toBe(7) // 'content' is 7 bytes
|
|
})
|
|
})
|
|
|
|
describe('fs - permissions', () => {
|
|
test('fs.chmod changes file permissions with octal number', () => {
|
|
const file = join(TEST_DIR, 'perms.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
fs.chmod(file, 0o755)
|
|
expect(fs['exec?'](file)).toBe(true)
|
|
|
|
fs.chmod(file, 0o644)
|
|
expect(fs['exec?'](file)).toBe(false)
|
|
})
|
|
|
|
test('fs.chmod changes file permissions with string', () => {
|
|
const file = join(TEST_DIR, 'perms2.txt')
|
|
writeFileSync(file, 'content')
|
|
|
|
fs.chmod(file, '755')
|
|
expect(fs['exec?'](file)).toBe(true)
|
|
|
|
fs.chmod(file, '644')
|
|
expect(fs['exec?'](file)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('fs - symlinks', () => {
|
|
test('fs.symlink creates symbolic link', () => {
|
|
const target = join(TEST_DIR, 'target.txt')
|
|
const link = join(TEST_DIR, 'link.txt')
|
|
writeFileSync(target, 'content')
|
|
|
|
fs.symlink(target, link)
|
|
expect(fs['symlink?'](link)).toBe(true)
|
|
expect(fs.read(link)).toBe('content')
|
|
})
|
|
|
|
test('fs.readlink reads symbolic link target', () => {
|
|
const target = join(TEST_DIR, 'target.txt')
|
|
const link = join(TEST_DIR, 'link.txt')
|
|
writeFileSync(target, 'content')
|
|
|
|
fs.symlink(target, link)
|
|
expect(fs.readlink(link)).toBe(target)
|
|
})
|
|
})
|
|
|
|
describe('fs - other', () => {
|
|
test('fs.glob matches file patterns', () => {
|
|
writeFileSync(join(TEST_DIR, 'file1.txt'), '')
|
|
writeFileSync(join(TEST_DIR, 'file2.txt'), '')
|
|
writeFileSync(join(TEST_DIR, 'file3.md'), '')
|
|
|
|
const result = fs.glob(join(TEST_DIR, '*.txt'))
|
|
expect(result).toHaveLength(2)
|
|
expect(result).toContain(join(TEST_DIR, 'file1.txt'))
|
|
expect(result).toContain(join(TEST_DIR, 'file2.txt'))
|
|
})
|
|
|
|
test('fs.watch calls callback on file change', async () => {
|
|
const file = join(TEST_DIR, 'watch.txt')
|
|
writeFileSync(file, 'initial')
|
|
|
|
let called = false
|
|
const watcher = fs.watch(file, () => { called = true })
|
|
|
|
// Trigger change
|
|
await new Promise(resolve => setTimeout(resolve, 100))
|
|
writeFileSync(file, 'updated')
|
|
|
|
// Wait for watcher
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
|
expect(called).toBe(true)
|
|
watcher.close?.()
|
|
})
|
|
}) |