Skip to content

Commit 854ca34

Browse files
authored
Merge branch 'main' into tech/elec_build_yml
2 parents cdda247 + c517a83 commit 854ca34

File tree

8 files changed

+186
-11
lines changed

8 files changed

+186
-11
lines changed

jest.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ module.exports = {
55
globals: {
66
'ts-jest': {}
77
},
8-
8+
testTimeout: 35000,
9+
setupFilesAfterEnv: ['./jest.setup.ts'],
910
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.config.js'],
1011
coverageDirectory: '<rootDir>/coverage',
1112
coveragePathIgnorePatterns: [

jest.setup.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
jest.setTimeout(35000)

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@
181181
"@types/graceful-fs": "^4.1.9",
182182
"@types/i18next-fs-backend": "^1.1.5",
183183
"@types/ini": "^1.3.34",
184-
"@types/jest": "^29.5.12",
184+
"@types/jest": "^29.5.14",
185185
"@types/jsdom": "^20.0.1",
186186
"@types/mime": "^3.0.2",
187187
"@types/node": "^20.16.2",
@@ -191,6 +191,7 @@
191191
"@types/react-blockies": "^1.4.1",
192192
"@types/react-dom": "^18.0.8",
193193
"@types/react-router-dom": "^5.3.3",
194+
"@types/rimraf": "^4.0.5",
194195
"@types/tmp": "^0.2.6",
195196
"@types/ws": "^8.5.12",
196197
"@typescript-eslint/eslint-plugin": "^6.21.0",
@@ -214,9 +215,10 @@
214215
"prettier": "^2.8.8",
215216
"pretty-quick": "^4.0.0",
216217
"react-markdown": "^9.0.1",
218+
"rimraf": "^6.0.1",
217219
"sass": "^1.55.0",
218220
"tmp": "^0.2.3",
219-
"ts-jest": "^29.1.1",
221+
"ts-jest": "^29.2.5",
220222
"type-fest": "^3.2.0",
221223
"typescript": "5.3.3",
222224
"vite": "^5.4.0",

pnpm-lock.yaml

+63-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/__tests__/utils.test.ts

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import * as utils from '../utils'
2-
import { getExecutableAndArgs } from '../utils'
2+
import { getExecutableAndArgs, copyRecursiveAsync } from '../utils'
3+
import { mkdir, writeFile, symlink } from 'fs/promises'
4+
import { join } from 'path'
5+
import { existsSync } from 'fs'
6+
import { rimraf } from 'rimraf'
7+
import os from 'os'
8+
import * as fs from 'fs'
39

410
jest.mock('electron')
511
jest.mock('../logger/logger')
@@ -306,4 +312,83 @@ describe('backend/utils.ts', () => {
306312
expect(getExecutableAndArgs(input)).toEqual(expected)
307313
})
308314
})
315+
316+
describe('copyRecursiveAsync', () => {
317+
const testDir = join(os.tmpdir(), `test-copy-${Date.now()}`)
318+
const sourceDir = join(testDir, 'source')
319+
const destDir = join(testDir, 'dest')
320+
321+
beforeEach(async () => {
322+
jest.useFakeTimers({ advanceTimers: true })
323+
await mkdir(sourceDir, { recursive: true })
324+
await mkdir(destDir, { recursive: true })
325+
})
326+
327+
afterEach(async () => {
328+
jest.clearAllTimers() // Clear pending timers
329+
jest.useRealTimers() // Restore real timers
330+
await rimraf(testDir)
331+
})
332+
333+
it('should copy a single file', async () => {
334+
const testFile = join(sourceDir, 'test.txt')
335+
const destFile = join(destDir, 'test.txt')
336+
await writeFile(testFile, 'test content')
337+
338+
await copyRecursiveAsync(testFile, destFile)
339+
340+
expect(existsSync(destFile)).toBe(true)
341+
})
342+
343+
it('should copy a directory recursively', async () => {
344+
const subDir = join(sourceDir, 'subdir')
345+
const testFile = join(subDir, 'test.txt')
346+
await mkdir(subDir, { recursive: true })
347+
await writeFile(testFile, 'test content')
348+
349+
await copyRecursiveAsync(sourceDir, join(destDir, 'source'))
350+
351+
expect(existsSync(join(destDir, 'source/subdir/test.txt'))).toBe(true)
352+
})
353+
354+
it('should skip symbolic links', async () => {
355+
const testFile = join(sourceDir, 'test.txt')
356+
const linkFile = join(sourceDir, 'link.txt')
357+
await writeFile(testFile, 'test content')
358+
await symlink(testFile, linkFile)
359+
360+
await copyRecursiveAsync(linkFile, join(destDir, 'link.txt'))
361+
362+
expect(existsSync(join(destDir, 'link.txt'))).toBe(false)
363+
})
364+
365+
it('should throw on timeout', async () => {
366+
const COPY_TIMEOUT_MS = 30000
367+
const testFile = join(sourceDir, 'test.txt')
368+
await writeFile(testFile, 'test content')
369+
370+
// Mock the copyFile function to simulate a slow operation
371+
const mockCopyFile = jest
372+
.spyOn(fs.promises, 'copyFile')
373+
.mockImplementation(async () => {
374+
return new Promise((resolve) => {
375+
setTimeout(resolve, COPY_TIMEOUT_MS + 1000)
376+
})
377+
})
378+
379+
const destFile = join(destDir, 'test.txt')
380+
381+
// Start the copy operation but don't await it yet
382+
const copyPromise = copyRecursiveAsync(testFile, destFile)
383+
384+
// Advance timers to trigger timeout
385+
jest.advanceTimersByTime(COPY_TIMEOUT_MS + 100)
386+
387+
// Now check if it throws
388+
await expect(copyPromise).rejects.toThrow('Timeout')
389+
390+
// Restore original implementation
391+
mockCopyFile.mockRestore()
392+
})
393+
})
309394
})

src/backend/ipcHandlers/mods.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { captureException } from '@sentry/electron'
12
import { notify, showDialogBoxModalAuto } from 'backend/dialog/dialog'
23
import { cancelQueueExtraction } from 'backend/downloadmanager/downloadqueue'
34
import { LogPrefix, logDebug, logError, logInfo } from 'backend/logger/logger'
@@ -233,7 +234,15 @@ export async function prepareBaseGameForModding({
233234
readdirSync(extractedFolderFullPath).forEach(async (file) => {
234235
const srcPath = path.join(extractedFolderFullPath, file)
235236
const destPath = path.join(dirPath, file)
236-
await copyRecursiveAsync(srcPath, destPath)
237+
try {
238+
await copyRecursiveAsync(srcPath, destPath)
239+
} catch (error) {
240+
const errorMessage = `Error copying ${srcPath} to ${destPath} ${error}`
241+
logError(errorMessage, LogPrefix.HyperPlay)
242+
extractService.emit('error', new Error(errorMessage))
243+
captureException(error)
244+
throw new Error(errorMessage)
245+
}
237246
})
238247

239248
// remove the extracted folder

src/backend/storeManagers/hyperplay/games.ts

+2
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ export async function install(
811811
const gameInfo = getGameInfo(appName)
812812
const { title, account_name } = gameInfo
813813
const isMarketWars = account_name === 'marketwars'
814+
814815
if (isMarketWars && modOptions?.zipFilePath) {
815816
try {
816817
await prepareBaseGameForModding({
@@ -819,6 +820,7 @@ export async function install(
819820
installPath: dirpath
820821
})
821822
} catch (error) {
823+
callAbortController(appName)
822824
return { status: 'error' }
823825
}
824826
}

src/backend/utils.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ import {
8080
deviceNameCache,
8181
vendorNameCache
8282
} from './utils/systeminfo/gpu/pci_ids'
83-
import { copyFile, mkdir, readdir, stat } from 'fs/promises'
83+
import { copyFile, lstat, mkdir, readdir } from 'fs/promises'
8484
import { GameConfig } from './game_config'
8585

8686
const execAsync = promisify(exec)
@@ -1334,9 +1334,16 @@ export const writeConfig = (appName: string, config: Partial<AppSettings>) => {
13341334
}
13351335
}
13361336

1337+
const COPY_TIMEOUT_MS = 30000 // wait time before throwing a timeout error
13371338
export async function copyRecursiveAsync(src: string, dest: string) {
1338-
const exists = (await stat(src)).isDirectory()
1339-
if (exists) {
1339+
const stats = await lstat(src)
1340+
if (stats.isSymbolicLink()) {
1341+
return // Skip symbolic links
1342+
}
1343+
1344+
const isDirectory = stats.isDirectory()
1345+
1346+
if (isDirectory) {
13401347
await mkdir(dest, { recursive: true })
13411348
const files = await readdir(src)
13421349
await Promise.all(
@@ -1347,6 +1354,13 @@ export async function copyRecursiveAsync(src: string, dest: string) {
13471354
})
13481355
)
13491356
} else {
1350-
await copyFile(src, dest)
1357+
await Promise.race([
1358+
copyFile(src, dest),
1359+
wait(COPY_TIMEOUT_MS).then(() => {
1360+
throw new Error(
1361+
`Timeout (${COPY_TIMEOUT_MS}ms) copying ${src} to ${dest}`
1362+
)
1363+
})
1364+
])
13511365
}
13521366
}

0 commit comments

Comments
 (0)