Skip to content

Commit

Permalink
added unit tests for report generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Mateusz Duda committed May 22, 2024
1 parent acf97fc commit c4ffc90
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 30 deletions.
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,13 @@ Publishing is made automatically by pushing a commit to the main branch, see [gi

The mock repo can be updated automatically by running `./pushTestRepo.sh`

### Features to do
### Features to do in order of importance

- [ ] Add keyboard shortcut hints when selecting tags and files -> https://github.com/enquirer/enquirer#select-choices
- [ ] Add groups on select prompt:
- [ ] Group files based on common path (files from same directory sould be grouped)
- [ ] Group tags based on parent modules
- [ ] Add remove hanging tags option to tag manager - search for tags not assigned to any module and ask the user if they want to delete them
- [ ] Unit tests for common actions:
- [ ] Find a way to clone test repository locally - this will make unit testing much quicker
- [ ] Unit tests for:
- [ ] The basic actions which can be performed on files - adding, deleting, modifying, renaming. After initial database entry the script should automatically handle all cases.
- [ ] Testing if files are correstly updated in database depending on changes in git
- [ ] On loading `tags.json` assert that all parents exist in database, if not then these modules won't be displayed
- [ ] Add unit tests for even the basic stuff - reading and parsing JSON files, synchronization between the database and repository, etc.
- [ ] Add unit tests for the basic actions which can be performed on files - adding, deleting, modifying, renaming. After initial database entry the script should automatically handle all cases.
- [ ] Add [adf-validator](https://github.com/torifat/adf-validator/tree/master) which would give more specific errors (right now comments are just not being posted)
- [ ] Reading and parsing JSON files, synchronization between the database and repository, etc.
- [ ] Add remove hanging tags option to tag manager - search for tags not assigned to any module and ask the user if they want to delete them
- [ ] Add keyboard shortcut hints when selecting tags and files -> https://github.com/enquirer/enquirer#select-choices
12 changes: 9 additions & 3 deletions src/FileSystem/fileSystemUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from "fs";
import path from "path";
import path, { join } from "path";

export function scopeFolderExists(root: string): boolean {
const scopeFolderPath = path.join(root, ".scope");
Expand Down Expand Up @@ -27,8 +27,14 @@ export function getFileDirectoryPath(filePath: string): string {
return filePath.substring(0, filePath.lastIndexOf("/"));
}

export function fileExists(filePath: string): boolean {
return fs.existsSync(filePath);
export function fileExists(filePath: string, relativeTo?: string): boolean {
let path = filePath;

if (relativeTo) {
path = join(relativeTo, filePath);
}

return fs.existsSync(path);
}

export function isDirectory(filePath: string): boolean {
Expand Down
4 changes: 4 additions & 0 deletions src/Git/GitRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,8 @@ export class GitRepository {
return false;
}
}

public get root() {
return this._root;
}
}
7 changes: 5 additions & 2 deletions src/References/TSReferenceFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ export class TSReferenceFinder implements IReferenceFinder {
const referenceList: Array<ReferencedFileInfo> = [];
const languageService = this._project.getLanguageService();

const sourceFile = this._project.getSourceFile(fileNameOrPath);
const allSourceFiles = this._project.getSourceFiles();

const pathRelativeToRoot = path.join(this._root, fileNameOrPath);
const sourceFile = this._project.getSourceFile(pathRelativeToRoot);

if (!sourceFile) {
console.log(`[TSReferenceFinder] File '${fileNameOrPath}' is not in scope of tsconfig.json of the project.`);
console.log(`[TSReferenceFinder] File '${pathRelativeToRoot}' is not in scope of tsconfig.json of the project.`);
return [];
}

Expand Down
4 changes: 3 additions & 1 deletion src/Report/ReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,16 @@ export class ReportGenerator {
private _getFileReferences(file: string, relevancy: Relevancy | null): Array<FileReference> {
const references: Array<FileReference> = [];

if (!fileExists(file)) {
// Command should be run outside of repo, so check path relative to repo
if (!fileExists(file, this._repository.root)) {
return references;
}

this._referenceFinders.forEach(referenceFinder => {
if (!referenceFinder.getSupportedFilesExtension().includes(getExtension(file))) {
return;
}

const foundReferences = referenceFinder.findReferences(file, relevancy);

foundReferences.forEach(reference => {
Expand Down
219 changes: 206 additions & 13 deletions test/report/reportGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TagsDefinitionFile } from "../../src/Scope/TagsDefinitionFile";
import { TSReferenceFinder } from "../../src/References/TSReferenceFinder";
import { ReportGenerator } from "../../src/Report/ReportGenerator";
import { RelevancyManager } from "../../src/Relevancy/RelevancyManager";
import { Relevancy } from "../../src/Relevancy/Relevancy";

// Testing only ReportGenerator class, which is responsible for gathering data for each commit

Expand Down Expand Up @@ -50,9 +51,7 @@ describe("Report generation works as expected", async () => {

expect(report).toBeDefined();

// { "path": "src/tagged-file.js",
// "tags": [
// { "tag": "Tag", "module": "Default module" }]}],
// src/tagged-file.js has 1 tag -> Default module / Tag

expect(report.allModules.length).toBe(1);
expect(report.untaggedFilesAsModule.files.length).toBe(0);
Expand Down Expand Up @@ -136,7 +135,6 @@ describe("Report generation works as expected", async () => {
});
});


it("Correctly separates tagged and untagged files in the report", async () => {
const FOLDER_PATH = makeUniqueFolderForTest();
const REPO_PATH = cloneMockRepositoryToFolder(FOLDER_PATH);
Expand Down Expand Up @@ -177,13 +175,12 @@ describe("Report generation works as expected", async () => {
* src/
* ├─ ts/
* │ ├─ controllers/
* │ │ ├─ Controller.ts 2 dependencies: View.ts and Model.ts, 1 tag: Controllers / AController
* │ │ ├─ Controller.ts 3 dependencies: View.ts and Model.ts, 1 tag: Controllers / AController
* │ ├─ models/
* │ │ ├─ Model.ts No dependencies, 1 tag: Models / AModel
* │ │ ├─ Model.ts 1 dependency: View.ts, 1 tag: Models / AModel
* │ ├─ views/
* │ │ ├─ View.ts 1 dependency: Modal, 1 tag: Views / AView
* │ │ ├─ ModalWindow.ts No dependencies, no tags
*
* │ │ ├─ View.ts 1 dependency: ModalWindow.ts, 1 tag: Views / AView
* │ │ ├─ ModalWindow.ts No dependencies, no tags
*/

const filesToModify = [
Expand Down Expand Up @@ -251,14 +248,210 @@ describe("Report generation works as expected", async () => {
// View.ts checks
const viewFileInfo = viewsModule.files[0];

expect(viewFileInfo.file).toBe("src/ts/view/View.ts");
expect(viewFileInfo.file).toBe("src/ts/views/View.ts");
expect(viewFileInfo.ignored).toBe(false);
expect(viewFileInfo.tagIdentifiers.length).toBeGreaterThan(0);
expect(viewFileInfo.tagIdentifiers.some(identifier => identifier.module === "Views" && identifier.tag === "AView")).toBe(true);

expect(viewFileInfo.usedIn.length).toBe(2);

const controllerDependency = viewFileInfo.usedIn.find(file => file.fileInfo.filename === "src/ts/controllers/Controller.ts");
const modelDependency = viewFileInfo.usedIn.find(file => file.fileInfo.filename === "src/ts/models/Model.ts");

expect(controllerDependency).toBeDefined();
expect(modelDependency).toBeDefined();

if (!controllerDependency || !modelDependency) {
return;
}

expect(controllerDependency.fileInfo.filename).toBe("src/ts/controllers/Controller.ts");
expect(controllerDependency.fileInfo.unused).toBe(false);

expect(modelDependency.fileInfo.filename).toBe("src/ts/models/Model.ts");
expect(modelDependency.fileInfo.unused).toBe(false);

// Check if ModalWindow is marked as untagged
expect(report.untaggedFilesAsModule.files.length).toBe(1);
expect(report.untaggedFilesAsModule.files[0].file).toBe("src/ts/view/ModalWindow.ts");
expect(report.untaggedFilesAsModule.files[0].file).toBe("src/ts/views/ModalWindow.ts");
expect(report.untaggedFilesAsModule.files[0].ignored).toBe(false);
expect(report.untaggedFilesAsModule.files[0].tagIdentifiers.length).toBe(0);

expect(report.untaggedFilesAsModule.files[0].usedIn.length).toBe(1);

const viewDependency = report.untaggedFilesAsModule.files[0].usedIn[0];

// Check references
expect(viewDependency.fileInfo.filename).toBe("src/ts/views/View.ts");
expect(viewDependency.fileInfo.unused).toBe(false);
expect(viewDependency.tagIdentifiers.some(identifier => identifier.module === "Views" && identifier.tag === "AView")).toBe(true);
});
});

it("Circular dependencies are reported correctly", async () => {
const FOLDER_PATH = makeUniqueFolderForTest();
const REPO_PATH = cloneMockRepositoryToFolder(FOLDER_PATH);

/**
* File structure description
*
* src/
* ├─ ts/
* │ ├─ circularDependency/
* │ │ ├─ ModuleA.ts 1 dependency: ModuleB.ts, 1 tag: Circular Dependency Test (Module A) / Module A
* │ │ ├─ ModuleB.ts 1 dependency: ModuleA.ts, 1 tag: Circular Dependency Test (Module B) / Module B
*/

const filesToModify = [
"src/ts/circularDependency/ModuleA.ts",
"src/ts/circularDependency/ModuleB.ts",
];

const repository = await commitModitication(filesToModify, REPO_PATH);

const unpushedCommits = await repository.getUnpushedCommits();
expect(unpushedCommits.length).toBe(1);

const commit = unpushedCommits[0];

const generator = initReportGenerator(REPO_PATH);

const relevancy = new RelevancyManager();
const relevancyMap = relevancy.loadRelevancyMapFromCommits([commit]);

const report = await generator.generateReportForCommit(commit, "Project", relevancyMap, true);

expect(report).toBeDefined();

expect(report.allModules.length).toBe(2);

const circularTestModuleA = report.allModules.find(reportModule => reportModule.module === "Circular Dependency Test (Module A)");
const circularTestModuleB = report.allModules.find(reportModule => reportModule.module === "Circular Dependency Test (Module B)");

expect(circularTestModuleA).toBeDefined();
expect(circularTestModuleB).toBeDefined();

if (!circularTestModuleA || !circularTestModuleB) {
return;
}

expect(circularTestModuleA.files.length).toBe(1);
expect(circularTestModuleA.files[0].file).toBe("src/ts/circularDependency/ModuleA.ts");
expect(circularTestModuleA.files[0].ignored).toBe(false);
expect(circularTestModuleA.files[0].tagIdentifiers.length).toBe(1);
expect(circularTestModuleA.files[0].tagIdentifiers.some(identifier => identifier.tag === "Module A" && identifier.module === "Circular Dependency Test (Module A)")).toBe(true);
expect(circularTestModuleA.files[0].usedIn.length).toBe(1);
expect(circularTestModuleA.files[0].usedIn[0].fileInfo.filename).toBe("src/ts/circularDependency/ModuleB.ts");
expect(circularTestModuleA.files[0].usedIn[0].fileInfo.unused).toBe(false);

expect(circularTestModuleB.files.length).toBe(1);
expect(circularTestModuleB.files[0].file).toBe("src/ts/circularDependency/ModuleB.ts");
expect(circularTestModuleB.files[0].ignored).toBe(false);
expect(circularTestModuleB.files[0].tagIdentifiers.length).toBe(1);
expect(circularTestModuleB.files[0].tagIdentifiers.some(identifier => identifier.tag === "Module B" && identifier.module === "Circular Dependency Test (Module B)")).toBe(true);
expect(circularTestModuleB.files[0].usedIn.length).toBe(1);
expect(circularTestModuleB.files[0].usedIn[0].fileInfo.filename).toBe("src/ts/circularDependency/ModuleA.ts");
expect(circularTestModuleB.files[0].usedIn[0].fileInfo.unused).toBe(false);

// Same for module B
});

it("Correctly reports files with multiple tags", async () => {

const FOLDER_PATH = makeUniqueFolderForTest();
const REPO_PATH = cloneMockRepositoryToFolder(FOLDER_PATH);

/**
Has following tags:
- Default module / Second
- Default module / Tag
- Default module 2 / Tag of default module 2
*/

const fileToModify = "src/tagged-file-with-multiple-modules.js";

const repository = await commitModitication([fileToModify], REPO_PATH);

const unpushedCommits = await repository.getUnpushedCommits();
expect(unpushedCommits.length).toBe(1);

const commit = unpushedCommits[0];

const generator = initReportGenerator(REPO_PATH);

const relevancy = new RelevancyManager();
const relevancyMap = relevancy.loadRelevancyMapFromCommits([commit]);

const report = await generator.generateReportForCommit(commit, "Project", relevancyMap, true);

expect(report).toBeDefined();
expect(report.allModules.length).toBe(2);
expect(report.untaggedFilesAsModule.files.length).toBe(0);

const defaultModule = report.allModules.find(reportModule => reportModule.module === "Default module");
const defaultModule2 = report.allModules.find(reportModule => reportModule.module === "Default module 2");

expect(defaultModule).toBeDefined();
expect(defaultModule2).toBeDefined();

if (!defaultModule || !defaultModule2) {
return;
}

expect(defaultModule.files.length).toBe(1);
expect(defaultModule.files[0].file).toBe("src/tagged-file-with-multiple-modules.js");
expect(defaultModule.files[0].ignored).toBe(false);
expect(defaultModule.files[0].usedIn.length).toBe(0);
// 3, because same FileInfo is shared between modules
expect(defaultModule.files[0].tagIdentifiers.length).toBe(3);
expect(defaultModule.files[0].tagIdentifiers.some(identifier => identifier.tag === "Tag" && identifier.module === "Default module")).toBe(true);
expect(defaultModule.files[0].tagIdentifiers.some(identifier => identifier.tag === "Second" && identifier.module === "Default module")).toBe(true);

expect(defaultModule2.files.length).toBe(1);
expect(defaultModule2.files[0].file).toBe("src/tagged-file-with-multiple-modules.js");
expect(defaultModule2.files[0].ignored).toBe(false);
expect(defaultModule2.files[0].usedIn.length).toBe(0);
// 3, because same FileInfo is shared between modules
expect(defaultModule2.files[0].tagIdentifiers.length).toBe(3);
expect(defaultModule2.files[0].tagIdentifiers.some(identifier => identifier.tag === "Tag of default module 2" && identifier.module === "Default module 2")).toBe(true);
});

it("After making a change with defined relevancy, the relevancy is present in generated report", async () => {
const FOLDER_PATH = makeUniqueFolderForTest();
const REPO_PATH = cloneMockRepositoryToFolder(FOLDER_PATH);

const repository = await commitModitication(
[
"src/tagged-file.js",
"src/untagged-file.js"
],
REPO_PATH,
`Automatic commit
__relevancy__[{"path":"src/tagged-file.js","relevancy":"HIGH","commit":"__current__"},{"path":"src/untagged-file.js","relevancy":"LOW","commit":"__current__"}]__relevancy__
`);

const unpushedCommits = await repository.getUnpushedCommits();
expect(unpushedCommits.length).toBe(1);

const commit = unpushedCommits[0];

const generator = initReportGenerator(REPO_PATH);

const relevancy = new RelevancyManager();
const relevancyMap = relevancy.loadRelevancyMapFromCommits([commit]);

const report = await generator.generateReportForCommit(commit, "Project", relevancyMap, true);

expect(report).toBeDefined();
expect(report.allModules.length).toBe(1);

const taggedModule = report.allModules[0];
expect(taggedModule.files.length).toBe(1);
expect(taggedModule.files[0].file).toBe("src/tagged-file.js");
expect(taggedModule.files[0].relevancy).toBe(Relevancy.HIGH);

expect(report.untaggedFilesAsModule.files.length).toBe(1);
expect(report.untaggedFilesAsModule.files[0].file).toBe("src/untagged-file.js");
expect(report.untaggedFilesAsModule.files[0].relevancy).toBe(Relevancy.LOW);
});
});

0 comments on commit c4ffc90

Please sign in to comment.