|
1 | 1 | import assert from 'node:assert/strict'; |
2 | 2 | import fs from 'node:fs'; |
3 | 3 | import path from 'node:path'; |
4 | | -import { test } from 'vitest'; |
| 4 | +import { test, vi } from 'vitest'; |
5 | 5 | import { AppError } from '@agent-device/kernel/errors'; |
6 | 6 | import type { DeviceInfo } from '@agent-device/kernel/device'; |
7 | 7 | import { mkdtempForTestSync } from './tmp-dir.ts'; |
8 | 8 | import { |
9 | 9 | acquireXcodebuildSimulatorSetRedirect, |
10 | | - releaseXcodebuildSimulatorSetRedirectBestEffort, |
11 | 10 | withXcodebuildSimulatorSetRedirect, |
12 | 11 | } from '../runner-device-set.ts'; |
13 | 12 |
|
@@ -127,7 +126,48 @@ test('a redirect handed back after its task keeps quiet about a release it canno |
127 | 126 | assert.notEqual(redirect, null); |
128 | 127 | makeReleaseUnverifiable(paths); |
129 | 128 |
|
130 | | - await releaseXcodebuildSimulatorSetRedirectBestEffort(redirect); |
| 129 | + await redirect?.releaseBestEffort(); |
131 | 130 | assert.equal(fs.existsSync(paths.lockDirPath), true); |
132 | 131 | }); |
133 | 132 | }); |
| 133 | + |
| 134 | +test('a redirect that could not restore the host device set reports it instead of swallowing it', async () => { |
| 135 | + await withTempDir('device-set-restore-failure-', async (root) => { |
| 136 | + const paths = makeRedirectPaths(root); |
| 137 | + fs.mkdirSync(paths.requestedSetPath, { recursive: true }); |
| 138 | + // The host has a device set of its own, so giving the redirect back renames it out of the |
| 139 | + // backup. Without this the release has nothing to restore and no rename to attempt. |
| 140 | + fs.mkdirSync(paths.xctestDeviceSetPath, { recursive: true }); |
| 141 | + fs.writeFileSync(path.join(paths.xctestDeviceSetPath, 'host-device.txt'), 'the host owns this'); |
| 142 | + const redirect = await acquireXcodebuildSimulatorSetRedirect(makeScopedSimulator(paths), { |
| 143 | + lockDirPath: paths.lockDirPath, |
| 144 | + xctestDeviceSetPath: paths.xctestDeviceSetPath, |
| 145 | + }); |
| 146 | + assert.notEqual(redirect, null); |
| 147 | + |
| 148 | + // The restore of the host's own `XCTestDevices` is a rename back from the backup, and a |
| 149 | + // refusal there is a fact about this machine that no caller may lose. |
| 150 | + let attempted = false; |
| 151 | + const realRename = fs.renameSync; |
| 152 | + const renameSpy = vi.spyOn(fs, 'renameSync').mockImplementation((( |
| 153 | + from: fs.PathLike, |
| 154 | + to: fs.PathLike, |
| 155 | + ) => { |
| 156 | + if (String(to) === paths.xctestDeviceSetPath && !attempted) { |
| 157 | + attempted = true; |
| 158 | + throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }); |
| 159 | + } |
| 160 | + return realRename(from as string, to as string); |
| 161 | + }) as typeof fs.renameSync); |
| 162 | + |
| 163 | + try { |
| 164 | + await assert.rejects( |
| 165 | + () => redirect!.releaseBestEffort(), |
| 166 | + (error: unknown) => (error as NodeJS.ErrnoException).code === 'EACCES', |
| 167 | + ); |
| 168 | + assert.equal(attempted, true); |
| 169 | + } finally { |
| 170 | + renameSpy.mockRestore(); |
| 171 | + } |
| 172 | + }); |
| 173 | +}); |
0 commit comments