mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-05-16 04:21:12 -04:00
604 lines
26 KiB
TypeScript
604 lines
26 KiB
TypeScript
// Copyright (c) 2026, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import {afterAll, beforeAll, describe, expect, it} from 'vitest';
|
|
|
|
import {
|
|
filterDisabled,
|
|
parseCompilersList,
|
|
parsePropertiesFileRaw,
|
|
type RawFileValidationResult,
|
|
type RawValidatorOptions,
|
|
validateCrossFileCompilerIds,
|
|
validateRawFile,
|
|
} from '../lib/properties-validator.js';
|
|
|
|
function validate(content: string, filename = 'test.properties', options?: RawValidatorOptions) {
|
|
return validateRawFile(parsePropertiesFileRaw(content, filename), options);
|
|
}
|
|
|
|
describe('Properties Validator', () => {
|
|
describe('parseCompilersList', () => {
|
|
it('should parse empty string', () => {
|
|
expect(parseCompilersList('')).toEqual([]);
|
|
});
|
|
|
|
it('should parse single id', () => {
|
|
expect(parseCompilersList('gcc')).toEqual(['gcc']);
|
|
});
|
|
|
|
it('should parse colon-separated ids', () => {
|
|
expect(parseCompilersList('gcc:clang:msvc')).toEqual(['gcc', 'clang', 'msvc']);
|
|
});
|
|
|
|
it('should filter empty elements', () => {
|
|
expect(parseCompilersList('gcc::clang')).toEqual(['gcc', 'clang']);
|
|
});
|
|
|
|
it('should handle group references', () => {
|
|
expect(parseCompilersList('&mygroup:gcc')).toEqual(['&mygroup', 'gcc']);
|
|
});
|
|
|
|
it('should handle remote references', () => {
|
|
expect(parseCompilersList('gcc@remote:clang')).toEqual(['gcc@remote', 'clang']);
|
|
});
|
|
});
|
|
|
|
describe('parsePropertiesFileRaw', () => {
|
|
it('should parse basic properties', () => {
|
|
const content = `
|
|
foo=bar
|
|
baz=qux
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.filename).toBe('test.properties');
|
|
expect(parsed.properties).toHaveLength(2);
|
|
expect(parsed.properties[0]).toEqual({key: 'foo', value: 'bar', line: 2});
|
|
expect(parsed.properties[1]).toEqual({key: 'baz', value: 'qux', line: 3});
|
|
});
|
|
|
|
it('should skip comments', () => {
|
|
const content = `
|
|
# This is a comment
|
|
foo=bar
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.properties).toHaveLength(1);
|
|
expect(parsed.properties[0].key).toBe('foo');
|
|
});
|
|
|
|
it('should parse Disabled: comments', () => {
|
|
const content = `
|
|
# Disabled: orphan1 orphan2
|
|
foo=bar
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.disabledIds).toContain('orphan1');
|
|
expect(parsed.disabledIds).toContain('orphan2');
|
|
});
|
|
|
|
it('should handle values with equals signs', () => {
|
|
const content = `options=-O2 -DFOO=bar`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.properties[0].value).toBe('-O2 -DFOO=bar');
|
|
});
|
|
|
|
it('should collect parse errors for invalid lines', () => {
|
|
const content = `
|
|
foo=bar
|
|
this is not valid
|
|
baz=qux
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.parseErrors).toHaveLength(1);
|
|
expect(parsed.parseErrors[0].line).toBe(3);
|
|
expect(parsed.parseErrors[0].text).toBe('this is not valid');
|
|
});
|
|
});
|
|
|
|
describe('duplicate key detection', () => {
|
|
it('should report duplicate keys', () => {
|
|
const result = validate('foo=bar\nfoo=baz');
|
|
expect(result.duplicateKeys).toHaveLength(1);
|
|
expect(result.duplicateKeys[0].id).toBe('foo');
|
|
});
|
|
|
|
it('should not report unique keys as duplicates', () => {
|
|
expect(validate('foo=bar\nbar=baz').duplicateKeys).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('empty list element detection', () => {
|
|
it.each([
|
|
['double colons in compilers', 'compilers=gcc::clang'],
|
|
['leading colons', 'compilers=:gcc'],
|
|
['trailing colons', 'compilers=gcc:'],
|
|
['empty elements in formatters', 'formatters=clangformat::rustfmt'],
|
|
['empty elements in tools', 'tools=readelf:nm:'],
|
|
])('should detect %s', (_, content) => {
|
|
expect(validate(content).emptyListElements).toHaveLength(1);
|
|
});
|
|
|
|
it('should not report valid compilers list', () => {
|
|
expect(validate('compilers=gcc:clang:msvc').emptyListElements).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('invalid property format detection', () => {
|
|
it('should report lines without equals sign', () => {
|
|
const result = validate('foo=bar\nthis is not valid\nbaz=qux');
|
|
expect(result.invalidPropertyFormat).toHaveLength(1);
|
|
expect(result.invalidPropertyFormat[0].text).toBe('this is not valid');
|
|
});
|
|
|
|
it('should not report valid properties', () => {
|
|
expect(validate('foo=bar\nbaz=qux').invalidPropertyFormat).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('typo detection', () => {
|
|
it('should detect compilers. instead of compiler.', () => {
|
|
const result = validate('compilers.gcc.exe=/path/to/gcc');
|
|
expect(result.typoCompilers).toHaveLength(1);
|
|
expect(result.typoCompilers[0].text).toContain('compilers.gcc');
|
|
});
|
|
|
|
it.each([
|
|
['valid compiler. properties', 'compiler.gcc.exe=/path/to/gcc'],
|
|
['compilers= list', 'compilers=gcc:clang'],
|
|
])('should not flag %s', (_, content) => {
|
|
expect(validate(content).typoCompilers).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('suspicious path detection', () => {
|
|
it('should flag paths outside standard locations', () => {
|
|
const result = validate('compiler.gcc.exe=/usr/bin/gcc', 'test.amazon.properties', {
|
|
checkSuspiciousPaths: true,
|
|
});
|
|
expect(result.suspiciousPaths).toHaveLength(1);
|
|
expect(result.suspiciousPaths[0].text).toBe('/usr/bin/gcc');
|
|
});
|
|
|
|
it.each([
|
|
['/opt/compiler-explorer paths', 'compiler.gcc.exe=/opt/compiler-explorer/gcc-12/bin/gcc'],
|
|
['Z:/compilers paths (Windows)', 'compiler.msvc.exe=Z:/compilers/msvc/cl.exe'],
|
|
])('should accept %s', (_, content) => {
|
|
expect(
|
|
validate(content, 'test.amazon.properties', {checkSuspiciousPaths: true}).suspiciousPaths,
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it.each([
|
|
['.defaults.properties files', 'c.defaults.properties'],
|
|
['.local.properties files', 'c.local.properties'],
|
|
])('should not check paths in %s', (_, filename) => {
|
|
expect(
|
|
validate('compiler.gcc.exe=/usr/bin/gcc', filename, {checkSuspiciousPaths: true}).suspiciousPaths,
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it('should not check paths when option is disabled', () => {
|
|
expect(
|
|
validate('compiler.gcc.exe=/usr/bin/gcc', 'test.amazon.properties', {checkSuspiciousPaths: false})
|
|
.suspiciousPaths,
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it.each([
|
|
['formatter paths', 'formatter.clangformat.exe=/usr/bin/clang-format'],
|
|
['tool paths', 'tools.readelf.exe=/usr/bin/readelf'],
|
|
])('should flag suspicious %s', (_, content) => {
|
|
expect(
|
|
validate(content, 'test.amazon.properties', {checkSuspiciousPaths: true}).suspiciousPaths,
|
|
).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('orphaned compiler detection', () => {
|
|
it('should report compilers listed but no .exe defined', () => {
|
|
const result = validate(`compilers=gcc:clang\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`);
|
|
expect(result.orphanedCompilerExe).toContainEqual(expect.objectContaining({id: 'clang'}));
|
|
});
|
|
|
|
it('should report compilers with .exe but not listed', () => {
|
|
const result = validate(
|
|
`compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc\ncompiler.clang.exe=/opt/compiler-explorer/clang/bin/clang`,
|
|
);
|
|
expect(result.orphanedCompilerExe).toContainEqual(expect.objectContaining({id: 'clang'}));
|
|
});
|
|
|
|
it('should not report when compilers match', () => {
|
|
const result = validate(
|
|
`compilers=gcc:clang\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc\ncompiler.clang.exe=/opt/compiler-explorer/clang/bin/clang`,
|
|
);
|
|
expect(result.orphanedCompilerExe).toHaveLength(0);
|
|
});
|
|
|
|
it('should ignore remote compiler references (with @)', () => {
|
|
const result = validate(`compilers=gcc:remote@host\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`);
|
|
expect(result.orphanedCompilerExe).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle alias expanding compilers', () => {
|
|
const result = validate(
|
|
`compilers=gcc:oldgcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc\nalias=oldgcc`,
|
|
);
|
|
expect(result.orphanedCompilerExe).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('orphaned group detection', () => {
|
|
it('should report groups referenced but not defined', () => {
|
|
expect(validate('compilers=&mygroup').orphanedGroups).toContainEqual(
|
|
expect.objectContaining({id: 'mygroup'}),
|
|
);
|
|
});
|
|
|
|
it('should accept groups that are defined', () => {
|
|
const result = validate(
|
|
`compilers=&mygroup\ngroup.mygroup.compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`,
|
|
);
|
|
expect(result.orphanedGroups).toHaveLength(0);
|
|
});
|
|
|
|
it('should report groups defined but not referenced', () => {
|
|
const result = validate(
|
|
`compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc\ngroup.unused.compilers=clang`,
|
|
);
|
|
expect(result.orphanedGroups).toContainEqual(expect.objectContaining({id: 'unused'}));
|
|
});
|
|
|
|
it('should handle nested group references', () => {
|
|
const result = validate(
|
|
`compilers=&outer\ngroup.outer.compilers=&inner\ngroup.inner.compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`,
|
|
);
|
|
expect(result.orphanedGroups).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('duplicated reference detection', () => {
|
|
it('should detect duplicate compiler references in same list', () => {
|
|
expect(validate('compilers=gcc:clang:gcc').duplicatedCompilerRefs).toContainEqual(
|
|
expect.objectContaining({id: 'gcc'}),
|
|
);
|
|
});
|
|
|
|
it('should detect duplicate group references', () => {
|
|
expect(validate('compilers=&mygroup:&mygroup').duplicatedGroupRefs).toContainEqual(
|
|
expect.objectContaining({id: 'mygroup'}),
|
|
);
|
|
});
|
|
|
|
it('should not flag unique references', () => {
|
|
expect(validate('compilers=gcc:clang:msvc').duplicatedCompilerRefs).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('orphaned formatter detection', () => {
|
|
it('should report formatters listed but not defined', () => {
|
|
const result = validate(
|
|
`formatters=clangformat:rustfmt\nformatter.clangformat.exe=/opt/compiler-explorer/clang-format`,
|
|
);
|
|
expect(result.orphanedFormatterExe).toContainEqual(expect.objectContaining({id: 'rustfmt'}));
|
|
});
|
|
|
|
it('should report formatters defined but not listed', () => {
|
|
const result = validate(
|
|
`formatters=clangformat\nformatter.clangformat.exe=/opt/compiler-explorer/clang-format\nformatter.rustfmt.exe=/opt/compiler-explorer/rustfmt`,
|
|
);
|
|
expect(result.orphanedFormatterExe).toContainEqual(expect.objectContaining({id: 'rustfmt'}));
|
|
});
|
|
});
|
|
|
|
describe('orphaned tool detection', () => {
|
|
it('should report tools listed but not defined', () => {
|
|
const result = validate(`tools=readelf:nm\ntools.readelf.exe=/usr/bin/readelf`);
|
|
expect(result.orphanedToolExe).toContainEqual(expect.objectContaining({id: 'nm'}));
|
|
});
|
|
|
|
it('should report tools defined but not listed', () => {
|
|
const result = validate(`tools=readelf\ntools.readelf.exe=/usr/bin/readelf\ntools.nm.exe=/usr/bin/nm`);
|
|
expect(result.orphanedToolExe).toContainEqual(expect.objectContaining({id: 'nm'}));
|
|
});
|
|
});
|
|
|
|
describe('orphaned library detection', () => {
|
|
it('should report libs listed but versions not defined', () => {
|
|
const result = validate(
|
|
`libs=boost:fmt\nlibs.boost.versions=1.80\nlibs.boost.versions.1.80.version=1.80.0`,
|
|
);
|
|
expect(result.orphanedLibIds).toContainEqual(expect.objectContaining({id: 'fmt'}));
|
|
});
|
|
|
|
it('should report lib versions listed but not defined', () => {
|
|
const result = validate(
|
|
`libs=boost\nlibs.boost.versions=1.80:1.81\nlibs.boost.versions.1.80.version=1.80.0`,
|
|
);
|
|
expect(result.orphanedLibVersions).toContainEqual(expect.objectContaining({id: 'boost 1.81'}));
|
|
});
|
|
|
|
it('should not report orphaned lib IDs when libs= is not present', () => {
|
|
const result = validate(`libs.boost.versions=1.80\nlibs.boost.versions.1.80.version=1.80.0`);
|
|
expect(result.orphanedLibIds).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('invalid default compiler detection', () => {
|
|
it('should report default compiler not in list', () => {
|
|
const result = validate(
|
|
`compilers=gcc:clang\ndefaultCompiler=msvc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc\ncompiler.clang.exe=/opt/compiler-explorer/clang`,
|
|
);
|
|
expect(result.invalidDefaultCompiler).toContainEqual(expect.objectContaining({id: 'msvc'}));
|
|
});
|
|
|
|
it('should accept valid default compiler', () => {
|
|
const result = validate(
|
|
`compilers=gcc:clang\ndefaultCompiler=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc\ncompiler.clang.exe=/opt/compiler-explorer/clang`,
|
|
);
|
|
expect(result.invalidDefaultCompiler).toHaveLength(0);
|
|
});
|
|
|
|
it('should report default compiler when there is no compilers list', () => {
|
|
const result = validate(`defaultCompiler=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc`);
|
|
expect(result.invalidDefaultCompiler).toContainEqual(expect.objectContaining({id: 'gcc'}));
|
|
});
|
|
});
|
|
|
|
describe('disabled allowlist', () => {
|
|
it('should filter disabled IDs from orphaned compilers', () => {
|
|
const content = `
|
|
# Disabled: orphan
|
|
compilers=gcc:orphan
|
|
compiler.gcc.exe=/opt/compiler-explorer/gcc
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
const result = validateRawFile(parsed);
|
|
const filtered = filterDisabled(result, parsed.disabledIds);
|
|
|
|
expect(result.orphanedCompilerExe).toContainEqual(expect.objectContaining({id: 'orphan'}));
|
|
expect(filtered.orphanedCompilerExe).not.toContainEqual(expect.objectContaining({id: 'orphan'}));
|
|
});
|
|
|
|
it('should handle multiple disabled IDs', () => {
|
|
const content = `
|
|
# Disabled: orphan1 orphan2
|
|
compilers=gcc:orphan1:orphan2
|
|
compiler.gcc.exe=/opt/compiler-explorer/gcc
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
const result = validateRawFile(parsed);
|
|
const filtered = filterDisabled(result, parsed.disabledIds);
|
|
|
|
expect(result.orphanedCompilerExe).toContainEqual(expect.objectContaining({id: 'orphan1'}));
|
|
expect(result.orphanedCompilerExe).toContainEqual(expect.objectContaining({id: 'orphan2'}));
|
|
expect(filtered.orphanedCompilerExe).not.toContainEqual(expect.objectContaining({id: 'orphan1'}));
|
|
expect(filtered.orphanedCompilerExe).not.toContainEqual(expect.objectContaining({id: 'orphan2'}));
|
|
});
|
|
|
|
it('should support Disable: variant spelling', () => {
|
|
const content = `
|
|
# Disable: orphan
|
|
compilers=orphan
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'test.properties');
|
|
|
|
expect(parsed.disabledIds).toContain('orphan');
|
|
});
|
|
});
|
|
|
|
describe('no compilers list detection', () => {
|
|
it('should flag language files with compiler definitions but no compilers=', () => {
|
|
expect(
|
|
validate(
|
|
`compiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc\ncompiler.gcc.name=GCC`,
|
|
'c++.amazon.properties',
|
|
).noCompilersList,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should not flag files with compilers=', () => {
|
|
expect(
|
|
validate(`compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`, 'c++.amazon.properties')
|
|
.noCompilersList,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should not flag files with group definitions', () => {
|
|
expect(
|
|
validate(
|
|
`group.mygroup.compilers=gcc\ncompiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`,
|
|
'c++.amazon.properties',
|
|
).noCompilersList,
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['defaults files', 'compiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc', 'c++.defaults.properties'],
|
|
['compiler-explorer config files', 'someOtherProperty=value', 'compiler-explorer.amazon.properties'],
|
|
['execution. files', 'compiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc', 'execution.amazon.properties'],
|
|
['aws. files', 'someProperty=value', 'aws.properties'],
|
|
['asm-docs. files', 'someProperty=value', 'asm-docs.properties'],
|
|
['builtin. files', 'someProperty=value', 'builtin.amazon.properties'],
|
|
['files with no compiler definitions', 'someProperty=value', 'c++.amazon.properties'],
|
|
])('should not flag %s', (_, content, filename) => {
|
|
expect(validate(content, filename).noCompilersList).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('cross-file compiler ID validation', () => {
|
|
it('should detect compiler IDs defined in multiple files', () => {
|
|
const file1Content = `
|
|
compiler.gcc.exe=/opt/compiler-explorer/gcc-12/bin/gcc
|
|
compiler.gcc.name=GCC 12
|
|
`;
|
|
const file2Content = `
|
|
compiler.gcc.exe=/opt/compiler-explorer/gcc-13/bin/gcc
|
|
compiler.gcc.name=GCC 13
|
|
`;
|
|
const parsed1 = parsePropertiesFileRaw(file1Content, 'c++.amazon.properties');
|
|
const parsed2 = parsePropertiesFileRaw(file2Content, 'c.amazon.properties');
|
|
|
|
const result = validateCrossFileCompilerIds([
|
|
{filename: 'c++.amazon.properties', parsed: parsed1},
|
|
{filename: 'c.amazon.properties', parsed: parsed2},
|
|
]);
|
|
|
|
expect(result.duplicateCompilerIds.has('gcc')).toBe(true);
|
|
expect(result.duplicateCompilerIds.get('gcc')).toHaveLength(2);
|
|
});
|
|
|
|
it('should not flag unique compiler IDs across files', () => {
|
|
const file1Content = `compiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc`;
|
|
const file2Content = `compiler.clang.exe=/opt/compiler-explorer/clang/bin/clang`;
|
|
|
|
const parsed1 = parsePropertiesFileRaw(file1Content, 'c++.amazon.properties');
|
|
const parsed2 = parsePropertiesFileRaw(file2Content, 'c.amazon.properties');
|
|
|
|
const result = validateCrossFileCompilerIds([
|
|
{filename: 'c++.amazon.properties', parsed: parsed1},
|
|
{filename: 'c.amazon.properties', parsed: parsed2},
|
|
]);
|
|
|
|
expect(result.duplicateCompilerIds.size).toBe(0);
|
|
});
|
|
|
|
it('should not count same ID multiple times within single file', () => {
|
|
const content = `
|
|
compiler.gcc.exe=/opt/compiler-explorer/gcc/bin/gcc
|
|
compiler.gcc.name=GCC
|
|
compiler.gcc.semver=12.0
|
|
`;
|
|
const parsed = parsePropertiesFileRaw(content, 'c++.amazon.properties');
|
|
|
|
const result = validateCrossFileCompilerIds([{filename: 'c++.amazon.properties', parsed}]);
|
|
|
|
expect(result.duplicateCompilerIds.size).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Real config validation', () => {
|
|
const configDir = 'etc/config';
|
|
const checkLocal = process.env.CHECK_LOCAL_PROPS === 'true';
|
|
let propertyFiles: Array<{filename: string; parsed: ReturnType<typeof parsePropertiesFileRaw>}> = [];
|
|
|
|
type ArrayFields = {
|
|
[K in keyof RawFileValidationResult]: RawFileValidationResult[K] extends Array<unknown> ? K : never;
|
|
}[keyof RawFileValidationResult];
|
|
|
|
function collectIssues(
|
|
files: typeof propertyFiles,
|
|
field: ArrayFields,
|
|
useFiltered = false,
|
|
options?: RawValidatorOptions,
|
|
) {
|
|
return files.flatMap(({filename, parsed}) => {
|
|
const result = validateRawFile(parsed, options);
|
|
const source = useFiltered ? filterDisabled(result, parsed.disabledIds) : result;
|
|
const issues = source[field];
|
|
if (issues.length > 0) {
|
|
return [{file: filename, issues: issues.map(i => i.id ?? i.text)}];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
beforeAll(() => {
|
|
const files = fs.readdirSync(configDir);
|
|
propertyFiles = files
|
|
.filter(f => {
|
|
if (!f.endsWith('.properties')) return false;
|
|
if (f.endsWith('.local.properties')) return checkLocal;
|
|
return true;
|
|
})
|
|
.map(filename => {
|
|
const content = fs.readFileSync(path.join(configDir, filename), 'utf8');
|
|
return {filename, parsed: parsePropertiesFileRaw(content, filename)};
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
propertyFiles = [];
|
|
});
|
|
|
|
it('should have property files to validate', () => {
|
|
expect(propertyFiles.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
const validationChecks: Array<{name: string; field: ArrayFields; useFiltered?: boolean}> = [
|
|
{name: 'duplicate keys', field: 'duplicateKeys', useFiltered: true},
|
|
{name: 'empty list elements', field: 'emptyListElements'},
|
|
{name: 'typo compilers', field: 'typoCompilers', useFiltered: true},
|
|
{name: 'invalid property format', field: 'invalidPropertyFormat'},
|
|
{name: 'orphaned compilers (exe)', field: 'orphanedCompilerExe', useFiltered: true},
|
|
{name: 'orphaned compilers (ID)', field: 'orphanedCompilerId', useFiltered: true},
|
|
{name: 'orphaned groups', field: 'orphanedGroups', useFiltered: true},
|
|
{name: 'orphaned formatters (exe)', field: 'orphanedFormatterExe', useFiltered: true},
|
|
{name: 'orphaned formatters (ID)', field: 'orphanedFormatterId', useFiltered: true},
|
|
{name: 'orphaned tools (exe)', field: 'orphanedToolExe', useFiltered: true},
|
|
{name: 'orphaned tools (ID)', field: 'orphanedToolId', useFiltered: true},
|
|
{name: 'orphaned lib IDs', field: 'orphanedLibIds', useFiltered: true},
|
|
{name: 'orphaned lib versions', field: 'orphanedLibVersions', useFiltered: true},
|
|
{name: 'duplicated compiler references', field: 'duplicatedCompilerRefs', useFiltered: true},
|
|
{name: 'duplicated group references', field: 'duplicatedGroupRefs', useFiltered: true},
|
|
{name: 'invalid default compilers', field: 'invalidDefaultCompiler', useFiltered: true},
|
|
];
|
|
|
|
it.each(validationChecks)('should have no $name', ({field, useFiltered}) => {
|
|
const filesWithIssues = collectIssues(propertyFiles, field, useFiltered);
|
|
expect(filesWithIssues, `Files with ${field}`).toEqual([]);
|
|
});
|
|
|
|
it('should have no duplicate compiler IDs across amazon property files', () => {
|
|
const amazonOnly = propertyFiles.filter(f => f.filename.includes('amazon'));
|
|
const result = validateCrossFileCompilerIds(amazonOnly);
|
|
if (result.duplicateCompilerIds.size > 0) {
|
|
const duplicates = Object.fromEntries(result.duplicateCompilerIds);
|
|
expect.fail(`Duplicate compiler IDs found: ${JSON.stringify(duplicates, null, 2)}`);
|
|
}
|
|
});
|
|
|
|
it('should have no language files missing compilers list', () => {
|
|
const filesWithMissingList = propertyFiles
|
|
.filter(({parsed}) => validateRawFile(parsed).noCompilersList)
|
|
.map(({filename}) => filename);
|
|
expect(filesWithMissingList, `Files missing compilers list`).toEqual([]);
|
|
});
|
|
|
|
it('should have no suspicious paths in amazon properties', () => {
|
|
const amazonFiles = propertyFiles.filter(f => f.filename.includes('amazon'));
|
|
const filesWithIssues = collectIssues(amazonFiles, 'suspiciousPaths', true, {checkSuspiciousPaths: true});
|
|
expect(filesWithIssues, 'Files with suspicious paths').toEqual([]);
|
|
});
|
|
});
|