rspec/frontend/src/deployment/__tests__/searchIndex.test.ts
Marco Antognini b2b116a8e2
RULEAPI-682: Index multiple types and rules with no languages
* Generate description and metadata for rules with no language, so that they get indexed.
* Index rules with different types in language specializations.
* Improve validation to reject new rules with no language specialization (i.e. only a predefined set of such rules is allowed because they were imported from Jira and kept for historical purposes).
* Write smaller JSON files, reduce their size by 30%.
* Improve test coverage of CLI application.
2022-01-28 09:51:13 +01:00

291 lines
11 KiB
TypeScript

import path from 'path';
import lunr from 'lunr';
import { buildSearchIndex, buildIndexStore, DESCRIPTION_SPLIT_REGEX } from '../searchIndex';
import { withTestDir, createFiles } from '../testutils';
import { IndexStore } from '../../types/IndexStore';
import {
addFilterForKeysTitlesDescriptions, addFilterForLanguages,
addFilterForQualityProfiles, addFilterForTags, addFilterForTypes
} from '../../utils/useSearch';
describe('index store generation', () => {
test('merges rules metadata', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
const ruleS3457 = indexStore['S3457'];
expect(ruleS3457).toMatchObject({
id: 'S3457',
types: ['CODE_SMELL'],
supportedLanguages: [
{ "name": "cfamily", "status": "ready", },
{ "name": "csharp", "status": "ready", },
{ "name": "default", "status": "ready", },
{ "name": "java", "status": "closed", },
{ "name": "python", "status": "deprecated", }
],
tags: ['cert', 'clumsy', 'confusing'],
severities: ['Major', 'Minor'],
qualityProfiles: ['MISRA C++ 2008 recommended', 'Sonar way'],
});
});
test('stores description words', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
const ruleS3457 = indexStore['S3457'];
const expectedWords = [
...'Because printf-style format strings are'.split(DESCRIPTION_SPLIT_REGEX),
...'Formatting strings, either with the % operator or'.split(DESCRIPTION_SPLIT_REGEX)
];
expect(ruleS3457.descriptions).toEqual(expect.arrayContaining(expectedWords));
});
test('computes types correctly', () => {
return withTestDir(async rulesPath => {
createFiles(rulesPath, {
'S100/default-metadata.json': JSON.stringify({
title: 'Rule S100',
type: 'CODE_SMELL',
}),
'S100/default-description.html': 'Description',
'S100/java-metadata.json': JSON.stringify({
title: 'Java Rule S100',
type: 'CODE_SMELL',
}),
'S100/java-description.html': 'Description',
'S101/default-metadata.json': JSON.stringify({
title: 'Rule S101',
type: 'CODE_SMELL',
}),
'S101/default-description.html': 'Description',
'S101/java-metadata.json': JSON.stringify({
title: 'Java Rule S101',
type: 'CODE_SMELL',
}),
'S101/java-description.html': 'Description',
'S101/python-metadata.json': JSON.stringify({
title: 'Rule S101',
type: 'VULNERABILITY',
}),
'S101/python-description.html': 'Description',
'S101/cfamily-metadata.json': JSON.stringify({
title: 'Rule S101',
type: 'BUG',
}),
'S101/cfamily-description.html': 'Description',
'S501/default-metadata.json': JSON.stringify({
title: 'Rule S501',
type: 'CODE_SMELL',
}),
'S501/default-description.html': 'Not implemented by any language',
});
const [indexStore, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.langs).toEqual({ 'cfamily': 1, 'default': 3, 'java': 2, 'python': 1 });
const ruleS100 = indexStore['S100'];
expect(ruleS100.types.sort()).toEqual(['CODE_SMELL']);
const ruleS101 = indexStore['S101'];
expect(ruleS101.types.sort()).toEqual(['BUG', 'CODE_SMELL', 'VULNERABILITY']);
const ruleS501 = indexStore['S501'];
expect(ruleS501.types.sort()).toEqual(['CODE_SMELL']);
const searchIndex = createIndex(indexStore);
expect(searchIndex.search('S501')).toHaveLength(1);
expect(searchIndex.search('titles:S501')).toHaveLength(1);
expect(searchIndex.search('titles:*')).toHaveLength(3);
expect(searchIndex.search('types:*')).toHaveLength(3);
// For types, the wildcard in the search query is required to succeed!
// This may be related to how tokenization is handled (or not done for arrays),
// but the actual reason doesn't matter for this test.
expect(searchIndex.search('BUG')).toHaveLength(1);
expect(searchIndex.search('types:BUG')).toHaveLength(1);
expect(searchIndex.search('*SMELL')).toHaveLength(3);
expect(searchIndex.search('types:*SMELL')).toHaveLength(3);
expect(searchIndex.search('*VULNERABILITY')).toHaveLength(1);
expect(searchIndex.search('types:*VULNERABILITY')).toHaveLength(1);
expect(findRulesByType(searchIndex, '*').sort()).toEqual(['S100', 'S101', 'S501']);
expect(findRulesByType(searchIndex, 'BUG').sort()).toEqual(['S101']);
expect(findRulesByType(searchIndex, 'CODE_SMELL').sort()).toEqual(['S100', 'S101', 'S501']);
expect(findRulesByType(searchIndex, 'VULNERABILITY').sort()).toEqual(['S101']);
});
});
test('collects all tags', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.tags).toEqual({"based-on-misra": 1,
"cert": 2,
"clumsy": 2,
"confusing": 1,
"lock-in": 1,
"misra-c++2008": 1,
"pitfall": 1
});
});
test('collects all languages', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.langs).toEqual({"cfamily": 3,
"csharp": 1,
"default": 3,
"java": 1,
"python": 1});
});
test('collects all rule keys', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
expect(indexStore['S3457'].all_keys).toEqual(['RSPEC-3457', 'S3457']);
expect(indexStore['S1000'].all_keys).toEqual(['RSPEC-1000', 'S1000', 'UnnamedNamespaceInHeader']);
expect(indexStore['S987'].all_keys).toEqual(['PPIncludeSignal', 'RSPEC-987', 'S987']);
});
test('collects all quality profiles', () => {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.qualityProfiles).toEqual({
"MISRA C++ 2008 recommended": 2,
"Sonar way": 2});
});
});
describe('search index enables search by title and description words', () => {
test('searches in rule keys', () => {
const searchIndex = createIndex();
const searchesS3457 = findRuleByQuery(searchIndex, 'S3457');
expect(searchesS3457).toEqual(['S3457']);
const searchesS987 = findRuleByQuery(searchIndex, 'ppincludesignal');
expect(searchesS987).toEqual(['S987']);
const searchesS1000 = findRuleByQuery(searchIndex, 'UnnamedNamespaceInHeader');
expect(searchesS1000).toEqual(['S1000']);
});
test('searches in rule description', () => {
const searchIndex = createIndex();
const searchesS3457 = findRuleByQuery(searchIndex, 'Because printf-style format');
expect(searchesS3457).toEqual(['S3457']);
const searchesS987 = findRuleByQuery(searchIndex, 'Signal handling contains');
expect(searchesS987).toEqual(['S987']);
const searchesUnknown = findRuleByQuery(searchIndex, 'Unknown description');
expect(searchesUnknown).toHaveLength(0);
const searchesBothRules = findRuleByQuery(searchIndex, 'Noncompliant Code Example');
expect(searchesBothRules).toEqual(['S1000', 'S3457', 'S987'].sort());
const searchesRuleMentions = findRuleByQuery(searchIndex, 'S1000');
expect(searchesRuleMentions).toEqual(['S987', 'S1000'].sort());
});
test('searches in rule title', () => {
const searchIndex = createIndex();
const searchesS3457 = findRuleByQuery(searchIndex, 'Composite format strings');
expect(searchesS3457).toEqual(['S3457']);
const searchesS987 = findRuleByQuery(searchIndex, 'signal.h used');
expect(searchesS987).toEqual(['S987']);
const searchesUnknown = findRuleByQuery(searchIndex, 'unknown title');
expect(searchesUnknown).toHaveLength(0);
const searchesBothRules = findRuleByQuery(searchIndex, 'should be used');
expect(searchesBothRules).toEqual(['S3457', 'S987'].sort());
});
});
describe('search index enables search by tags, quality profiles and languages', () => {
test('searches in rule tags', () => {
const searchIndex = createIndex();
const searchesS3457 = findRulesByTags(searchIndex, ['cert']);
expect(searchesS3457).toHaveLength(2);
expect(searchesS3457).toContain('S1000');
expect(searchesS3457).toContain('S3457');
const searchesS987 = findRulesByTags(searchIndex, ['based-on-misra', 'lock-in']);
expect(searchesS987).toEqual(['S987']);
const searchesUnknown = findRulesByTags(searchIndex, ['unknown tag']);
expect(searchesUnknown).toHaveLength(0);
});
test('searches in rule quality profiles', () => {
const searchIndex = createIndex();
const searchesSonarWay = findRulesByProfile(searchIndex, 'sonar way');
expect(searchesSonarWay).toEqual(['S1000', 'S3457']);
const filtersAll = findRulesByProfile(searchIndex, 'non-existent');
expect(filtersAll).toEqual([]);
});
test('filter per language', () => {
const searchIndex = createIndex();
const csharpRules = findRulesByLanguage(searchIndex, 'csharp');
expect(csharpRules).toEqual(['S3457']);
const cfamilyRules = findRulesByLanguage(searchIndex, 'cfamily');
expect(cfamilyRules.sort()).toEqual(['S987', 'S1000', 'S3457'].sort());
});
});
function createIndex(ruleIndexStore?: IndexStore) {
if (!ruleIndexStore) {
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
ruleIndexStore = indexStore;
}
// Hack to avoid warnings when 'selectivePipeline' is already registered
if ('selectivePipeline' in (lunr.Pipeline as any).registeredFunctions) {
delete (lunr.Pipeline as any).registeredFunctions['selectivePipeline']
}
return buildSearchIndex(ruleIndexStore);
}
function findRules<QueryParam>(
index: lunr.Index,
filter: (q: lunr.Query, param: QueryParam) => void,
param: QueryParam
): string[] {
const hits = index.query(q => filter(q, param));
return hits.map(({ ref }) => ref);
}
function findRulesByType(index: lunr.Index, type: string): string[] {
return findRules(index, addFilterForTypes, type);
}
function findRulesByTags(index: lunr.Index, tags: string[]): string[] {
return findRules(index, addFilterForTags, tags);
}
function findRulesByLanguage(index: lunr.Index, language: string): string[] {
return findRules(index, addFilterForLanguages, language);
}
function findRulesByProfile(index: lunr.Index, profile: string): string[] {
return findRules(index, addFilterForQualityProfiles, [profile]);
}
function findRuleByQuery(index: lunr.Index, query: string): string[] {
const rules = findRules(index, addFilterForKeysTitlesDescriptions, query);
return rules.sort();
}