Skip to content

Commit a2b81b1

Browse files
authored
Feature/quick scan (#5)
1 parent 348c1eb commit a2b81b1

19 files changed

Lines changed: 746 additions & 157 deletions

.github/workflows/build-and-release.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ name: Build and Release
22

33
on:
44
push:
5-
branches: ["main", "develop"]
5+
branches: ["main", "develop", "feature/*"]
66

77
jobs:
88
build-windows:
9-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
9+
if: github.event_name == 'push' || (github.ref_name == 'develop' || github.ref_name == 'main' || startsWith(github.ref_name, 'feature/'))
1010
runs-on: windows-latest
1111

1212
outputs:
@@ -67,7 +67,7 @@ jobs:
6767
dist/DevNullifier-win-unpacked.zip
6868
6969
build-linux:
70-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
70+
if: github.event_name == 'push' || (github.ref_name == 'develop' || github.ref_name == 'main' || startsWith(github.ref_name, 'feature/'))
7171
runs-on: ubuntu-latest
7272

7373
steps:
@@ -109,7 +109,7 @@ jobs:
109109
dist/DevNullifier-linux-unpacked.zip
110110
111111
build-macos:
112-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
112+
if: github.event_name == 'push' || (github.ref_name == 'develop' || github.ref_name == 'main' || startsWith(github.ref_name, 'feature/'))
113113
runs-on: macos-latest
114114

115115
steps:
@@ -148,7 +148,7 @@ jobs:
148148
dist/DevNullifier-mac-unpacked.zip
149149
150150
create-release:
151-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
151+
if: github.event_name == 'push' && (github.ref_name == 'develop' || github.ref_name == 'main')
152152
runs-on: ubuntu-latest
153153
needs: [build-windows, build-linux, build-macos]
154154

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to DevNullifier will be documented in this file.
44

5+
## [1.2.3] - Quick scan
6+
7+
### Added
8+
9+
- **Quick Scan**: scan only folders found during full scan
10+
511
## [1.2.2] - Dark Mode
612

713
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "devnullifier",
3-
"version": "1.2.2",
3+
"version": "1.2.3",
44
"description": "DevNullifier: Clean application data, dev caches (node_modules, .cache, Library, Binary, Intermediate, etc) with Electron, Vue 3, and Vuetify 3.",
55
"main": "dist-main/main/main.js",
66
"scripts": {

src/main/__tests__/appDataScanWorker.test.ts

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { promises as fs } from "fs";
22
import { vi, describe, it, expect, beforeEach } from "vitest";
33
import { getDirSize } from "../fileUtils";
4-
import { FolderScanner, type IMessagePort, type WorkerResponse } from "../appDataScanWorker";
4+
import { FolderScanner, initializeWorker, type IMessagePort, type WorkerResponse } from "../appDataScanWorker";
5+
import path from "path";
56

67
// Mock modules
78
vi.mock("fs", () => ({
@@ -32,6 +33,11 @@ describe("FolderScanner", () => {
3233
mockMessagePort = {
3334
postMessage: (msg: WorkerResponse) => {
3435
messages.push(msg);
36+
},
37+
on: (event: "message", listener: (message: WorkerResponse) => void) => {
38+
if (event === "message") {
39+
listener(messages[messages.length - 1]);
40+
}
3541
}
3642
};
3743

@@ -58,19 +64,26 @@ describe("FolderScanner", () => {
5864
] as any);
5965

6066
const results = await scanner.scanPaths(paths, 2, keywords);
67+
scanner.sendFolders(true); // Force send any remaining folders
6168

6269
expect(results).toHaveLength(2);
6370
expect(messages).toContainEqual({ type: "current-path", path: paths[0] });
6471
expect(messages).toContainEqual({ type: "current-path", path: paths[1] });
65-
expect(messages).toContainEqual({
66-
type: "folder-found",
67-
folder: expect.objectContaining({ name: "cache-dir" })
68-
});
69-
expect(messages).toContainEqual({
70-
type: "folder-found",
71-
folder: expect.objectContaining({ name: "temp-dir" })
72-
});
73-
expect(messages).toContainEqual({ type: "progress", count: 1 });
72+
73+
// Find all folder-found messages
74+
const folderFoundMessages = messages.filter(m => m.type === "folder-found");
75+
expect(folderFoundMessages).toHaveLength(1); // We expect one final batched message
76+
77+
// Verify the folders in the message - order doesn't matter since they're batched
78+
const foundFolders = folderFoundMessages[0].folders;
79+
expect(foundFolders).toHaveLength(2);
80+
expect(foundFolders).toEqual(
81+
expect.arrayContaining([
82+
expect.objectContaining({ name: "cache-dir", size: 1234 }),
83+
expect.objectContaining({ name: "temp-dir", size: 1234 })
84+
])
85+
);
86+
7487
expect(messages).toContainEqual({ type: "progress", count: 2 });
7588
expect(messages).toContainEqual({ type: "done", results });
7689
});
@@ -140,9 +153,89 @@ describe("FolderScanner", () => {
140153
] as any);
141154

142155
const results = await scanner.scanPaths(["/test"], 1, ["cache"]);
156+
scanner.sendFolders(true); // Force send any remaining folders
143157

144158
expect(results).toHaveLength(3);
145-
expect(messages.filter(m => m.type === "folder-found")).toHaveLength(3);
159+
// Since folders are now batched, we expect one message with all 3 folders
160+
const folderFoundMessages = messages.filter(m => m.type === "folder-found");
161+
expect(folderFoundMessages).toHaveLength(1);
162+
163+
// Verify all folders are in the final batched message
164+
const foundFolders = folderFoundMessages[0].folders;
165+
expect(foundFolders).toHaveLength(3);
166+
expect(foundFolders).toEqual(
167+
expect.arrayContaining([
168+
expect.objectContaining({ name: "CACHE", size: 1234 }),
169+
expect.objectContaining({ name: "Cache", size: 1234 }),
170+
expect.objectContaining({ name: "cache", size: 1234 })
171+
])
172+
);
173+
});
174+
175+
it("should handle error when path normalization fails", async () => {
176+
// Mock path.normalize to throw
177+
const originalNormalize = path.normalize;
178+
path.normalize = vi.fn().mockImplementation(() => {
179+
throw new Error("Invalid path");
180+
});
181+
182+
try {
183+
const results = await scanner.scanPaths(["/test/path"], 1, ["cache"]);
184+
185+
expect(messages).toEqual([
186+
{
187+
type: "error",
188+
error: "Invalid path"
189+
}
190+
]);
191+
192+
expect(results).toEqual([]);
193+
} finally {
194+
// Restore original normalize function
195+
path.normalize = originalNormalize;
196+
}
197+
});
198+
199+
it("initialize worker without parentPort throws error", () => {
200+
expect(() => initializeWorker()).toThrowError("Worker thread parent port not available");
201+
});
202+
203+
it("initialize worker with injected port", () => {
204+
let functions: any[] = [];
205+
const port = {
206+
postMessage: (message: WorkerResponse) => {
207+
messages.push(message);
208+
},
209+
on: (event: string, listener: (message: WorkerResponse) => void) => {
210+
functions.push(listener);
211+
}
212+
} as unknown as IMessagePort;
213+
initializeWorker(port);
214+
expect(functions).toHaveLength(1);
215+
expect(functions[0]).toBeDefined();
216+
217+
// call function message.paths, message.maxDepth, message.keywords
218+
functions[0]({
219+
type: "message",
220+
data: {
221+
paths: ["/test"],
222+
maxDepth: 1,
223+
keywords: ["cache"]
224+
}
225+
});
226+
227+
expect(messages).toContainEqual({
228+
"error": "paths is not iterable",
229+
"type": "error"
230+
});
231+
232+
functions[0]({
233+
type: "stop",
234+
});
235+
236+
expect(messages).toContainEqual({
237+
"type": "stop"
238+
});
146239
});
147240
});
148241
});

src/main/__tests__/developerScanWorker.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,56 @@ describe("developerScanWorker", () => {
293293
expect(Array.isArray(result)).toBe(true);
294294
});
295295

296+
it("should continue scanning subdirectories when project has no caches", async () => {
297+
// Mock initial directory read
298+
vi
299+
.mocked(fs.readdir)
300+
.mockResolvedValueOnce(
301+
[
302+
mockDirEntry("package.json", false),
303+
mockDirEntry("subproject", true)
304+
] as any
305+
) // Root dir
306+
.mockResolvedValueOnce([mockDirEntry("package.json", false)] as any); // Subproject dir
307+
308+
// Mock calculateCacheSizes to return no caches for root project but some for subproject
309+
const mockCalculateCacheSizes = vi.spyOn(worker, "calculateCacheSizes");
310+
mockCalculateCacheSizes
311+
.mockResolvedValueOnce({ caches: [], totalSize: 0 }) // Root project
312+
.mockResolvedValueOnce({
313+
caches: [
314+
{
315+
pattern: "node_modules",
316+
category: "TestProject",
317+
matches: [],
318+
totalSize: 1000,
319+
selectedSize: 0,
320+
expanded: false
321+
}
322+
],
323+
totalSize: 1000
324+
}); // Subproject
325+
326+
// Mock fs.stat
327+
vi.mocked(fs.stat).mockResolvedValue(
328+
{
329+
isDirectory: () => true,
330+
mtime: new Date(),
331+
size: 1000
332+
} as any
333+
);
334+
335+
const result = await worker.scanDeveloperProjects(
336+
["/test/path"],
337+
[mockCategory]
338+
);
339+
340+
// Should find one project (the subproject)
341+
expect(result).toHaveLength(1);
342+
expect(result[0].path).toContain("/test/path");
343+
expect(result[0].totalCacheSize).toBe(2000);
344+
});
345+
296346
it("should handle scan errors gracefully", async () => {
297347
// Mock fs.readdir to throw error
298348
vi.mocked(fs.readdir).mockRejectedValue(new Error("Test error"));

src/main/__tests__/dirSize.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { promises as fs } from "fs";
1+
import { promises as fs, existsSync } from "fs";
22
import path from "path";
33
import os from "os";
44
import { describe, it, expect, beforeEach, afterEach } from "vitest";
@@ -10,6 +10,9 @@ describe("getDirectorySize", () => {
1010
beforeEach(async () => {
1111
// Create a temporary directory
1212
tempDir = path.join(os.tmpdir(), `test-${Date.now()}`);
13+
if (existsSync(tempDir)) {
14+
await fs.rm(tempDir, { recursive: true, force: true });
15+
}
1316
await fs.mkdir(tempDir);
1417
});
1518

@@ -36,7 +39,7 @@ describe("getDirectorySize", () => {
3639
it("should calculate size of directory with nested directories", async () => {
3740
// Create nested directory structure
3841
const subDir = path.join(tempDir, "subdir");
39-
await fs.mkdir(subDir);
42+
await fs.mkdir(subDir, { recursive: true }); // Ensure subdirectory exists
4043

4144
// Create files in both root and nested directory
4245
const rootContent = "a".repeat(100);

0 commit comments

Comments
 (0)