Adds indexingService tests, refactors

This commit is contained in:
Simon Schödler 2023-05-20 13:19:59 +02:00
parent 2254182da4
commit 508bb1637b
4 changed files with 196 additions and 7 deletions

View file

@ -107,6 +107,9 @@ export default class CrossbowPlugin extends Plugin {
private onFileRename = (file: TAbstractFile, oldPath: string): void => {
this.loggingService.debugLog(`⚡File renamed. '${file.name}'`);
this.indexingService.clearCacheFromFile(oldPath);
// TODO: Verify if we could just use: this.indexingService.indexFile(file as TFile);$
// Could be problematic since file is TAbstractFile
this.app.metadataCache.trigger('changed', file as TFile, ''); // Trigger metadata change to update cache
};

View file

@ -0,0 +1,184 @@
// Copyright (C) 2023 - shoedler - github.com/shoedler
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
import { CachedMetadata, FileStats, HeadingCache, TFile, TFolder, TagCache, Vault } from 'obsidian';
import { CrossbowIndexingService } from './indexingService';
import { CrossbowLoggingService } from './loggingService';
import { CrossbowSettingsService, DEFAULT_SETTINGS } from './settingsService';
const proto = CrossbowIndexingService.prototype;
describe('indexingService', () => {
describe(`${proto.indexFile.name}()`, () => {
it('should generate cache entries for headings, tags and one for the file itself', () => {
const fileName = 'testFile';
const file = createFileMock(fileName);
const metadata = createMetadataCacheMock();
const service = createServiceMock();
service.indexFile(file, metadata);
const cache = service.getCache();
// Key count
expect(Object.keys(cache)).toHaveLength(5);
expect(Object.values(cache).filter((value) => value.type === 'File')).toHaveLength(1);
expect(Object.values(cache).filter((value) => value.type === 'Heading')).toHaveLength(3);
expect(Object.values(cache).filter((value) => value.type === 'Tag')).toHaveLength(1);
expect(Object.keys(getSourceLookup(service))).toHaveLength(1);
// Should contain file cache entry
expect(cache[fileName]).toBeDefined();
expect(cache[fileName].type).toBe('File');
if (metadata.headings === undefined) {
throw new Error('Metadata headings are undefined');
}
// Should contain heading cache entries
metadata.headings.forEach((headingCache) => {
expect(cache[headingCache.heading]).toBeDefined();
expect(cache[headingCache.heading].type).toBe('Heading');
expect(cache[headingCache.heading].file).toBe(file);
expect(cache[headingCache.heading].text).toBe(headingCache.heading);
});
if (metadata.tags === undefined) {
throw new Error('Metadata tags are undefined');
}
// Should contain tag cache entries
metadata.tags.forEach((tagCache) => {
expect(cache[tagCache.tag]).toBeDefined();
expect(cache[tagCache.tag].type).toBe('Tag');
expect(cache[tagCache.tag].file).toBe(file);
expect(cache[tagCache.tag].text).toBe(tagCache.tag);
});
});
it('should not duplicate cache entries when file cache is provided', () => {
const file = createFileMock('testFile');
const metadata = createMetadataCacheMock();
const service = createServiceMock();
service.indexFile(file, metadata);
expect(Object.keys(service.getCache())).toHaveLength(5);
service.indexFile(file, metadata);
expect(Object.keys(service.getCache())).toHaveLength(5);
});
});
describe(`${proto.clearCacheFromFile.name}()`, () => {
it('should clear cache entries (headings, tags and the file)', () => {
const file: TFile = createFileMock('testFile');
const metadata = createMetadataCacheMock();
const service = createServiceMock();
service.indexFile(file, metadata);
expect(Object.keys(service.getCache())).toHaveLength(5);
expect(Object.keys(getSourceLookup(service))).toHaveLength(1);
service.clearCacheFromFile(file);
expect(Object.keys(service.getCache())).toHaveLength(0);
expect(Object.keys(getSourceLookup(service))).toHaveLength(0);
});
it("shouldn't clear cache entries (headings, tags and the file) of other files", () => {
const file = createFileMock('testFile');
const metadata = createMetadataCacheMock();
const service = createServiceMock();
service.indexFile(file, metadata);
expect(Object.keys(service.getCache())).toHaveLength(5);
expect(Object.keys(getSourceLookup(service))).toHaveLength(1);
service.clearCacheFromFile(file);
expect(Object.keys(service.getCache())).toHaveLength(0);
expect(Object.keys(getSourceLookup(service))).toHaveLength(0);
});
});
describe(`${proto.clearCache.name}()`, () => {
it('should clear cache (and sourceFileLookup)', () => {
const fileName1 = 'testFile';
const file1 = createFileMock(fileName1);
const metadata1 = createMetadataCacheMock();
const fileName2 = 'testFile2';
const file2 = createFileMock(fileName2);
const metadata2 = createMetadataCacheMock();
const service = createServiceMock();
service.indexFile(file1, metadata1);
service.indexFile(file2, metadata2);
expect(Object.keys(service.getCache())).toHaveLength(10);
expect(Object.keys(getSourceLookup(service))).toHaveLength(2);
service.clearCache();
expect(Object.keys(service.getCache())).toHaveLength(5);
expect(Object.keys(getSourceLookup(service))).toHaveLength(1);
});
});
});
const settingsServiceMock = new CrossbowSettingsService((settings) => Promise.resolve());
const loggingService = new CrossbowLoggingService(settingsServiceMock);
settingsServiceMock.saveSettings(DEFAULT_SETTINGS);
const createServiceMock = (): CrossbowIndexingService =>
new CrossbowIndexingService(settingsServiceMock, loggingService);
const getSourceLookup = (service: CrossbowIndexingService): object => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (service as any).sourceFileLookup as object;
};
const createFileMock = (fileName: string): TFile => ({
basename: fileName,
name: `${fileName}Name`,
path: `./${fileName}`,
extension: 'md',
stat: null as unknown as FileStats,
vault: null as unknown as Vault,
parent: null as unknown as TFolder,
});
const createMetadataCacheMock = (headingsCount = 3, tagsCount = 1): CachedMetadata => {
const headings: HeadingCache[] = [];
for (let index = 0; index < headingsCount; index++) {
headings.push({
level: index + 1,
heading: `Heading ${index + 1}`,
position: {
start: { line: index, col: 0, offset: 0 },
end: { line: index, col: 8 + index.toString().length, offset: 0 },
},
});
}
const tags: TagCache[] = [];
for (let index = 0; index < tagsCount; index++) {
tags.push({
tag: `Tag ${index + 1}`,
position: {
start: { line: index + headingsCount, col: 0, offset: 0 },
end: { line: index + headingsCount, col: 5 + index.toString().length, offset: 0 },
},
});
}
return { headings, tags };
};

View file

@ -29,8 +29,8 @@ export interface CacheMatch extends CacheEntry {
}
export class CrossbowIndexingService {
private readonly crossbowCache: CacheEntryLookup = {};
private readonly sourceFileLookup: SourceLookup = {};
private crossbowCache: CacheEntryLookup = {};
private sourceFileLookup: SourceLookup = {};
public constructor(
private readonly settingsService: CrossbowSettingsService,
@ -109,7 +109,7 @@ export class CrossbowIndexingService {
}
public clearCache(): void {
Object.assign(this.crossbowCache, {});
Object.assign(this.sourceFileLookup, {});
this.crossbowCache = {};
this.sourceFileLookup = {};
}
}

View file

@ -15,8 +15,10 @@
import { Editor } from 'obsidian';
import { CrossbowTokenizationService, WordLookup } from './tokenizationService';
const proto = CrossbowTokenizationService.prototype;
describe('tokenizationService', () => {
describe('redactText()', () => {
describe(`${CrossbowTokenizationService.redactText.name}()`, () => {
it('should redact code blocks (```) from a string, leaving spaces in its place', () => {
const input___ = 'This is a ```\ncode block\n``` string';
const expected = 'This is a \n \n string';
@ -90,7 +92,7 @@ describe('tokenizationService', () => {
});
});
describe('cleanWord()', () => {
describe(`${CrossbowTokenizationService.cleanWord.name}()`, () => {
it('should remove anything but alphanumeric chars from a word', () => {
const input = 'Word$¨\'^!"*ç"*ç%&/()=?`*';
const expected = 'Word';
@ -99,7 +101,7 @@ describe('tokenizationService', () => {
});
});
describe('getWordLookupFromEditor()', () => {
describe(`${proto.getWordLookupFromEditor.name}()`, () => {
const mockEditor = (value: string) => {
return {
value,