storebound/tests/path-free.test.ts

104 lines
6.8 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { archiveScanDirs, derivedDataFor, leakNeedles, leakProblem, outsideHome, pathFreeSettings, prefixMaps, scanForPaths } from '../cli/lib/path-free';
import { archiveArgs, validateAppleBuild } from '../cli/lib/xcode';
const resolve = (p: string): string => (p.startsWith('/') ? p : `/proj/${p}`);
const t = { framework: 'native' as const, project: 'App.xcodeproj', scheme: 'App', notes: [] };
describe('derived data', () => {
test('flag beats config beats <out>/DerivedData; without a chosen out Xcode keeps its default', () => {
expect(derivedDataFor({ flag: '/dd', config: 'cfg', out: '/o', outChosen: true, resolve })).toBe('/dd');
expect(derivedDataFor({ config: 'cfg/dd', out: '/o', outChosen: true, resolve })).toBe('/proj/cfg/dd');
expect(derivedDataFor({ out: '/shared/ci/out', outChosen: true, resolve })).toBe('/shared/ci/out/DerivedData');
expect(derivedDataFor({ out: '/proj/build/ios', outChosen: false, resolve })).toBeNull();
});
test('archive gets -derivedDataPath only when set', () => {
const base = { target: t, root: '/p', configuration: 'Release', archivePath: '/o/App.xcarchive', build: '1', auth: null };
const withDd = archiveArgs({ ...base, derivedData: '/o/DerivedData' });
expect(withDd.slice(withDd.indexOf('-archivePath'), withDd.indexOf('-archivePath') + 4)).toEqual(['-archivePath', '/o/App.xcarchive', '-derivedDataPath', '/o/DerivedData']);
expect(archiveArgs(base)).not.toContain('-derivedDataPath');
expect(archiveArgs({ ...base, derivedData: null })).not.toContain('-derivedDataPath');
});
test('config keys are validated', () => {
expect(validateAppleBuild({ derivedData: 'dd', pathFree: true })).toEqual([]);
expect(validateAppleBuild({ derivedData: ' ' })).toHaveLength(1);
expect(validateAppleBuild({ pathFree: 'yes' as unknown as boolean })).toHaveLength(1);
});
test('outsideHome', () => {
expect(outsideHome('/srv/shared/ci/out', '/Users/you')).toBe(true);
expect(outsideHome('/Users/you/proj/out', '/Users/you')).toBe(false);
expect(outsideHome('/Users/you-too/out', '/Users/you')).toBe(true);
});
});
describe('prefix maps', () => {
const i = { home: '/Users/you', root: '/Users/you/work/app', out: '/Users/you/work/app/build/ios', derivedData: '/Users/you/work/app/build/ios/DerivedData' };
test('longest first (swiftc takes the first match; clang gets the list reversed)', () => {
expect(prefixMaps(i).map(m => m[0])).toEqual(['/Users/you/work/app/build/ios/DerivedData', '/Users/you/work/app/build/ios', '/Users/you/work/app', '/Users/you']);
});
test('duplicates and a root slash are dropped', () => {
expect(prefixMaps({ home: '/h/', root: '/h', out: '/h', derivedData: null }).map(m => m[0])).toEqual(['/h']);
});
test('settings carry swift and clang flags and keep $(inherited)', () => {
const s = pathFreeSettings(i);
expect(s.map(x => x.split('=')[0])).toEqual(['SWIFT_SERIALIZE_DEBUGGING_OPTIONS', 'OTHER_SWIFT_FLAGS', 'OTHER_CFLAGS', 'OTHER_CPLUSPLUSFLAGS']);
expect(s[0]!).toBe('SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO');
expect(s[1]!).toContain('$(inherited) -debug-prefix-map /Users/you/work/app/build/ios/DerivedData=/derived -file-prefix-map /Users/you/work/app/build/ios/DerivedData=/derived');
expect(s[1]!.endsWith('-file-prefix-map /Users/you=/home')).toBe(true);
expect(s[1]!.indexOf('app=.')).toBeLessThan(s[1]!.indexOf('/Users/you=/home'));
// clang: the last match wins, so the home folder comes first
expect(s[2]!.indexOf('-ffile-prefix-map=/Users/you=/home')).toBeLessThan(s[2]!.indexOf('-ffile-prefix-map=/Users/you/work/app/build/ios/DerivedData=/derived'));
expect(s.join()).not.toContain('DEBUG_INFORMATION_FORMAT');
});
test('a path with spaces is quoted', () => {
const s = pathFreeSettings({ home: '/Users/you', root: '/Users/you/Visual Studio Workspaces/app', out: '/Users/you/o' });
expect(s[1]!).toContain('"/Users/you/Visual Studio Workspaces/app=."');
expect(s[2]!).toContain('"-ffile-prefix-map=/Users/you/Visual Studio Workspaces/app=."');
});
});
describe('leak scan', () => {
const dir = mkdtempSync(join(tmpdir(), 'pf-'));
const arch = join(dir, 'App.xcarchive');
mkdirSync(join(arch, 'Products/Applications/App.app'), { recursive: true });
mkdirSync(join(arch, 'dSYMs/App.app.dSYM'), { recursive: true });
mkdirSync(join(arch, 'Symbols'), { recursive: true });
writeFileSync(join(arch, 'Products/Applications/App.app/App'), Buffer.concat([Buffer.from([0, 1, 2]), Buffer.from('/Users/you/work/app/Sources/A.swift'), Buffer.from([0]), Buffer.from('/Users/you/Library/Developer/Xcode/DerivedData/x')]));
writeFileSync(join(arch, 'dSYMs/App.app.dSYM/clean'), 'DW_AT_comp_dir ./Sources /Applications/Xcode.app/Contents');
writeFileSync(join(arch, 'Symbols/0.symbols'), 'path /Users/you/work/app/B.swift');
const needles = leakNeedles({ home: '/Users/you', user: 'you', root: '/Users/you/work/app' });
test('needles are named by kind; /Users/<user>/ is dropped when it equals the home folder', () => {
expect(needles.map(n => n.kind)).toEqual(['home folder', 'checkout']);
});
test('a shared build folder outside the home folder is not a needle (dsymutil / module cache paths there are fine)', () => {
expect(leakNeedles({ home: '/Users/you', user: 'you', root: '/Users/Shared/ship/app/abc' }).map(n => n.kind)).toEqual(['home folder']);
expect(leakNeedles({ home: '/Users/you', user: 'you', root: '/opt/builds/you/app' }).map(n => n.kind)).toEqual(['home folder', 'checkout']);
});
test('counts hits, keeps first offsets per file, scans app, dSYMs and Symbols', () => {
const r = scanForPaths(archiveScanDirs(arch), needles);
expect(r.files).toBe(2);
expect(r.total).toBeGreaterThanOrEqual(5);
const app = r.hits.filter(h => h.file === 'Applications/App.app/App');
expect(app.length).toBe(3);
expect(app[0]!.offset).toBe(3);
expect(r.hits.some(h => h.file === '0.symbols')).toBe(true);
expect(r.hits.some(h => h.file.includes('clean'))).toBe(false);
});
test('the report has counts, kinds, files and offsets but never the paths', () => {
const msg = leakProblem(scanForPaths(archiveScanDirs(arch), needles), 'build/App.xcarchive')!;
expect(msg).toContain('local paths left in 2 files');
expect(msg).toContain('Applications/App.app/App @3: home folder');
expect(msg).not.toContain('/Users/you');
});
test('a clean archive has no problem', () => {
const clean = join(dir, 'Clean.xcarchive');
mkdirSync(join(clean, 'Products'), { recursive: true });
writeFileSync(join(clean, 'Products/a'), 'x /Applications/Xcode.app/y');
expect(leakProblem(scanForPaths(archiveScanDirs(clean), needles), 'c')).toBeNull();
rmSync(dir, { recursive: true, force: true });
});
});