import { Disposable, DisposableCollection, } from '@theia/core/lib/common/disposable'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { expect } from 'chai'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import temp from 'temp'; import { clangFormatFilename, ClangFormatter, } from '../../node/clang-formatter'; import { spawnCommand } from '../../node/exec-util'; import { createBaseContainer, startDaemon } from './node-test-bindings'; const unformattedContent = `void setup ( ) { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite( LED_BUILTIN , HIGH ); delay( 1000 ) ; digitalWrite( LED_BUILTIN , LOW); delay ( 1000 ) ; } `; const formattedContent = `void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(1000); digitalWrite(LED_BUILTIN, LOW); delay(1000); } `; type ClangStyleValue = | string | number | boolean | ClangStyleValue[] | { [key: string]: ClangStyleValue }; type ClangConfiguration = Record; export interface ClangStyle { readonly key: string; readonly value: ClangStyleValue; } const singleClangStyles: ClangStyle[] = [ { key: 'SpacesBeforeTrailingComments', value: 0, }, { key: 'SortIncludes', value: 'Never', }, { key: 'AlignTrailingComments', value: true, }, { key: 'IfMacros', value: ['KJ_IF_MAYBE'], }, { key: 'SpacesInLineCommentPrefix', value: { Minimum: 0, Maximum: -1, }, }, ]; async function expectNoChanges( formatter: ClangFormatter, styleArg: string ): Promise { const minimalContent = ` void setup() {} void loop() {} `.trim(); const execPath = formatter['execPath'](); const actual = await spawnCommand( execPath, ['-style', styleArg], console.error, minimalContent ); expect(actual).to.be.equal(minimalContent); } describe('clang-formatter', () => { let tracked: typeof temp; let formatter: ClangFormatter; let toDispose: DisposableCollection; before(async () => { tracked = temp.track(); toDispose = new DisposableCollection( Disposable.create(() => tracked.cleanupSync()) ); const container = await createBaseContainer({ additionalBindings: (bind) => bind(ClangFormatter).toSelf().inSingletonScope(), }); await startDaemon(container, toDispose); formatter = container.get(ClangFormatter); }); after(() => toDispose.dispose()); singleClangStyles .map((style) => ({ ...style, styleArg: JSON.stringify({ [style.key]: style.value }), })) .map(({ value, styleArg }) => it(`should execute the formatter with a single ${ Array.isArray(value) ? 'array' : typeof value } type style configuration value: ${styleArg}`, async () => { await expectNoChanges(formatter, styleArg); }) ); it('should execute the formatter with a multiple clang formatter styles', async () => { const styleArg = JSON.stringify( singleClangStyles.reduce((config, curr) => { config[curr.key] = curr.value; return config; }, {} as ClangConfiguration) ); await expectNoChanges(formatter, styleArg); }); it('should format with the default styles', async () => { const actual = await formatter.format({ content: unformattedContent, formatterConfigFolderUris: [], }); expect(actual).to.be.equal(formattedContent); }); it('should format with custom formatter configuration file', async () => { const tempPath = tracked.mkdirSync(); await fs.writeFile( path.join(tempPath, clangFormatFilename), 'SpaceInEmptyParentheses: true', { encoding: 'utf8', } ); const actual = await formatter.format({ content: 'void foo() {}', formatterConfigFolderUris: [FileUri.create(tempPath).toString()], }); expect(actual).to.be.equal('void foo( ) {}'); }); });