Stop hanging after KiCad folder permission.
Apri progetto walked every nested file (HubAudio .history ~2000) so onFilesChange never ran and Next stayed disabled. Read only kicad_pro, sch, and pcb in the project dir; timeout to an error; enable Next when .kicad_pro is in.
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.60.9 — 2026-09-21 — Apri progetto does not walk forever
|
||||||
|
|
||||||
|
After folder permission, 2.60.8 kept `Next` disabled because `showDirectoryPicker` then recursed `dirHandle.entries()` / `getFile()` through the whole tree (HubAudio `.history` is ~2000 files). We only read the project directory: `.kicad_pro`, `.kicad_sch`, `.kicad_pcb`. Timeout shows an error instead of a spinner forever. Next enables when a `.kicad_pro` is in.
|
||||||
|
|
||||||
|
- [Fixed] Folder pick no longer hangs on nested dirs / permission / giant trees.
|
||||||
|
- [Fixed] Next on step 1 when `.kicad_pro` is present.
|
||||||
|
|
||||||
## 2.60.8 — 2026-09-21 — Folder picker, not Carica file dialog
|
## 2.60.8 — 2026-09-21 — Folder picker, not Carica file dialog
|
||||||
|
|
||||||
Apri progetto opened the Mac file list (**Carica**, no row selected) because the control was a React `<input type="file" accept="">` without a working `webkitdirectory`. Empty Carica then looked like “no .kicad_pro”. It now uses `showDirectoryPicker` (Chrome) or a hidden input created with `webkitdirectory=true` and **no accept**. Cancel/empty pick is ignored.
|
Apri progetto opened the Mac file list (**Carica**, no row selected) because the control was a React `<input type="file" accept="">` without a working `webkitdirectory`. Empty Carica then looked like “no .kicad_pro”. It now uses `showDirectoryPicker` (Chrome) or a hidden input created with `webkitdirectory=true` and **no accept**. Cancel/empty pick is ignored.
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import type {
|
|||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
|
detailsStepReady,
|
||||||
filesBesideKicadPro,
|
filesBesideKicadPro,
|
||||||
looksLikeKicadPro,
|
looksLikeKicadPro,
|
||||||
looksLikeKicadSch,
|
looksLikeKicadSch,
|
||||||
@@ -635,6 +636,7 @@ export function CreateProjectDialog({
|
|||||||
const [netlistUploadedEarly, setNetlistUploadedEarly] = useState(false);
|
const [netlistUploadedEarly, setNetlistUploadedEarly] = useState(false);
|
||||||
const [earlyUploading, setEarlyUploading] = useState(false);
|
const [earlyUploading, setEarlyUploading] = useState(false);
|
||||||
const [earlyUploadError, setEarlyUploadError] = useState<string | null>(null);
|
const [earlyUploadError, setEarlyUploadError] = useState<string | null>(null);
|
||||||
|
const [folderBusy, setFolderBusy] = useState(false);
|
||||||
|
|
||||||
// EDIF sub-design state, populated from the early netlist upload response.
|
// EDIF sub-design state, populated from the early netlist upload response.
|
||||||
// `selectedSubdesignIds = null` means "include every sub-design" (default
|
// `selectedSubdesignIds = null` means "include every sub-design" (default
|
||||||
@@ -1715,10 +1717,7 @@ export function CreateProjectDialog({
|
|||||||
|
|
||||||
const stepReady = (): boolean => {
|
const stepReady = (): boolean => {
|
||||||
if (step === "details") {
|
if (step === "details") {
|
||||||
const kicadReady =
|
return detailsStepReady(name, netlistFiles) && !folderBusy;
|
||||||
netlistFiles.some((f) => looksLikeKicadPro(f)) &&
|
|
||||||
netlistFiles.some((f) => looksLikeKicadSch(f));
|
|
||||||
return !!(name.trim() && kicadReady && !netlistError);
|
|
||||||
}
|
}
|
||||||
if (step === "columns") return !!(refCol && mpnCol);
|
if (step === "columns") return !!(refCol && mpnCol);
|
||||||
if (step === "subdesigns")
|
if (step === "subdesigns")
|
||||||
@@ -1748,6 +1747,12 @@ export function CreateProjectDialog({
|
|||||||
|
|
||||||
const advanceStep = async () => {
|
const advanceStep = async () => {
|
||||||
if (step === "details") {
|
if (step === "details") {
|
||||||
|
if (!netlistFiles.some((f) => looksLikeKicadSch(f))) {
|
||||||
|
setNetlistError(
|
||||||
|
"Manca lo schematico nella stessa cartella del .kicad_pro.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// EDIF: eagerly upload so we can show the sub-design picker before
|
// EDIF: eagerly upload so we can show the sub-design picker before
|
||||||
// the user reaches Run. Skip in rerun mode (netlist is already on
|
// the user reaches Run. Skip in rerun mode (netlist is already on
|
||||||
// the server) and when already done.
|
// the server) and when already done.
|
||||||
@@ -2126,6 +2131,10 @@ export function CreateProjectDialog({
|
|||||||
directory
|
directory
|
||||||
files={netlistFiles}
|
files={netlistFiles}
|
||||||
onFilesChange={onKicadProjectFolder}
|
onFilesChange={onKicadProjectFolder}
|
||||||
|
onPickState={({ busy, error }) => {
|
||||||
|
setFolderBusy(busy);
|
||||||
|
if (error) setNetlistError(error);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{netlistError ? (
|
{netlistError ? (
|
||||||
<p className="text-sm text-rose-600 dark:text-rose-400 leading-tight flex items-start gap-1">
|
<p className="text-sm text-rose-600 dark:text-rose-400 leading-tight flex items-start gap-1">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState, type ChangeEvent, type DragEvent, type MouseEvent } from "react";
|
import { useCallback, useState, type ChangeEvent, type DragEvent, type MouseEvent } from "react";
|
||||||
import { FileCheck, Upload } from "lucide-react";
|
import { FileCheck, Loader2, Upload } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { isKicadProjectFileName, skipWalkDirName } from "@/lib/kicad-project-files";
|
||||||
|
|
||||||
export type FileUploadZoneProps = {
|
export type FileUploadZoneProps = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -12,9 +13,12 @@ export type FileUploadZoneProps = {
|
|||||||
files: File[];
|
files: File[];
|
||||||
onFilesChange: (files: File[]) => void;
|
onFilesChange: (files: File[]) => void;
|
||||||
preloaded?: string[];
|
preloaded?: string[];
|
||||||
|
onPickState?: (state: { busy: boolean; error: string | null }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKIP_DIR = /(-backups|\.pretty|3dmodels|__macosx)$/i;
|
const WALK_MS = 8_000;
|
||||||
|
const WALK_TIMEOUT_IT =
|
||||||
|
"La cartella è troppo grande o il browser non finisce di leggerla. Scegli solo la cartella del .kicad_pro, non il repo intero.";
|
||||||
|
|
||||||
function relativePath(file: File): string {
|
function relativePath(file: File): string {
|
||||||
return (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
|
return (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
|
||||||
@@ -32,6 +36,22 @@ function stampRelativePath(file: File, rel: string): File {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = window.setTimeout(() => reject(new Error(message)), ms);
|
||||||
|
promise.then(
|
||||||
|
(value) => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readAllEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
|
function readAllEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const collected: FileSystemEntry[] = [];
|
const collected: FileSystemEntry[] = [];
|
||||||
@@ -53,21 +73,24 @@ function fileFromEntry(entry: FileSystemFileEntry): Promise<File> {
|
|||||||
return new Promise((resolve, reject) => entry.file(resolve, reject));
|
return new Promise((resolve, reject) => entry.file(resolve, reject));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Project folder only: kicad_pro / sch / pcb. Do not recurse (HubAudio .history is ~2000 files). */
|
||||||
async function flattenEntry(root: FileSystemEntry): Promise<File[]> {
|
async function flattenEntry(root: FileSystemEntry): Promise<File[]> {
|
||||||
const out: File[] = [];
|
const out: File[] = [];
|
||||||
const stack: { entry: FileSystemEntry; prefix: string }[] = [{ entry: root, prefix: "" }];
|
if (root.isFile) {
|
||||||
while (stack.length > 0) {
|
if (!isKicadProjectFileName(root.name)) return out;
|
||||||
const { entry, prefix } = stack.pop()!;
|
const file = await fileFromEntry(root as FileSystemFileEntry);
|
||||||
if (entry.isFile) {
|
out.push(stampRelativePath(file, file.name));
|
||||||
const file = await fileFromEntry(entry as FileSystemFileEntry);
|
return out;
|
||||||
out.push(stampRelativePath(file, prefix + file.name));
|
}
|
||||||
continue;
|
if (!root.isDirectory) return out;
|
||||||
}
|
if (skipWalkDirName(root.name)) return out;
|
||||||
if (!entry.isDirectory) continue;
|
const kids = await readAllEntries((root as FileSystemDirectoryEntry).createReader());
|
||||||
if (SKIP_DIR.test(entry.name)) continue;
|
const nestedPrefix = root.name + "/";
|
||||||
const kids = await readAllEntries((entry as FileSystemDirectoryEntry).createReader());
|
for (const child of kids) {
|
||||||
const nestedPrefix = prefix + entry.name + "/";
|
if (child.isDirectory) continue;
|
||||||
for (const child of kids) stack.push({ entry: child, prefix: nestedPrefix });
|
if (!isKicadProjectFileName(child.name)) continue;
|
||||||
|
const file = await fileFromEntry(child as FileSystemFileEntry);
|
||||||
|
out.push(stampRelativePath(file, nestedPrefix + child.name));
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -81,12 +104,14 @@ async function filesFromDropEvent(event: DragEvent): Promise<File[]> {
|
|||||||
if (entry) fromEntries.push(...(await flattenEntry(entry)));
|
if (entry) fromEntries.push(...(await flattenEntry(entry)));
|
||||||
else {
|
else {
|
||||||
const fallback = item.getAsFile();
|
const fallback = item.getAsFile();
|
||||||
if (fallback) fromEntries.push(fallback);
|
if (fallback && isKicadProjectFileName(fallback.name)) fromEntries.push(fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fromEntries.length > 0) return fromEntries;
|
if (fromEntries.length > 0) return fromEntries;
|
||||||
}
|
}
|
||||||
return Array.from(event.dataTransfer?.files ?? []);
|
return Array.from(event.dataTransfer?.files ?? []).filter((f) =>
|
||||||
|
isKicadProjectFileName(relativePath(f)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeSelection(current: File[], incoming: File[], multiple: boolean, directory: boolean): File[] {
|
function mergeSelection(current: File[], incoming: File[], multiple: boolean, directory: boolean): File[] {
|
||||||
@@ -102,13 +127,11 @@ type DirHandle = {
|
|||||||
async function filesFromDirectoryHandle(handle: DirHandle, prefix = ""): Promise<File[]> {
|
async function filesFromDirectoryHandle(handle: DirHandle, prefix = ""): Promise<File[]> {
|
||||||
const out: File[] = [];
|
const out: File[] = [];
|
||||||
for await (const [name, child] of handle.entries()) {
|
for await (const [name, child] of handle.entries()) {
|
||||||
if (SKIP_DIR.test(name)) continue;
|
if (child.kind === "directory") continue;
|
||||||
if (child.kind === "file" && child.getFile) {
|
if (!isKicadProjectFileName(name)) continue;
|
||||||
const file = await child.getFile();
|
if (!child.getFile) continue;
|
||||||
out.push(stampRelativePath(file, prefix + name));
|
const file = await child.getFile();
|
||||||
} else if (child.kind === "directory") {
|
out.push(stampRelativePath(file, prefix + name));
|
||||||
out.push(...(await filesFromDirectoryHandle(child, prefix + name + "/")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -123,7 +146,7 @@ function pickFolderViaHiddenInput(): Promise<File[]> {
|
|||||||
input.style.display = "none";
|
input.style.display = "none";
|
||||||
const finish = (files: File[]) => {
|
const finish = (files: File[]) => {
|
||||||
input.remove();
|
input.remove();
|
||||||
resolve(files);
|
resolve(files.filter((f) => isKicadProjectFileName(relativePath(f))));
|
||||||
};
|
};
|
||||||
input.addEventListener("change", () => finish(Array.from(input.files ?? [])));
|
input.addEventListener("change", () => finish(Array.from(input.files ?? [])));
|
||||||
input.addEventListener("cancel", () => finish([]));
|
input.addEventListener("cancel", () => finish([]));
|
||||||
@@ -139,12 +162,17 @@ async function pickProjectFolder(): Promise<File[]> {
|
|||||||
if (typeof picker === "function") {
|
if (typeof picker === "function") {
|
||||||
try {
|
try {
|
||||||
const handle = await picker.call(window);
|
const handle = await picker.call(window);
|
||||||
return await filesFromDirectoryHandle(handle, `${handle.name}/`);
|
return await withTimeout(
|
||||||
|
filesFromDirectoryHandle(handle, `${handle.name}/`),
|
||||||
|
WALK_MS,
|
||||||
|
WALK_TIMEOUT_IT,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException && err.name === "AbortError") return [];
|
if (err instanceof DOMException && err.name === "AbortError") return [];
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pickFolderViaHiddenInput();
|
return withTimeout(pickFolderViaHiddenInput(), WALK_MS, WALK_TIMEOUT_IT);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileUploadZone({
|
export function FileUploadZone({
|
||||||
@@ -155,20 +183,37 @@ export function FileUploadZone({
|
|||||||
files,
|
files,
|
||||||
onFilesChange,
|
onFilesChange,
|
||||||
preloaded,
|
preloaded,
|
||||||
|
onPickState,
|
||||||
}: FileUploadZoneProps) {
|
}: FileUploadZoneProps) {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
const filled = files.length > 0 || Boolean(preloaded && preloaded.length > 0);
|
const filled = files.length > 0 || Boolean(preloaded && preloaded.length > 0);
|
||||||
|
|
||||||
|
const report = useCallback(
|
||||||
|
(nextBusy: boolean, error: string | null) => {
|
||||||
|
setBusy(nextBusy);
|
||||||
|
onPickState?.({ busy: nextBusy, error });
|
||||||
|
},
|
||||||
|
[onPickState],
|
||||||
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(event: DragEvent) => {
|
(event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setHover(false);
|
setHover(false);
|
||||||
void filesFromDropEvent(event).then((dropped) => {
|
report(true, null);
|
||||||
if (dropped.length === 0) return;
|
void withTimeout(filesFromDropEvent(event), WALK_MS, WALK_TIMEOUT_IT)
|
||||||
onFilesChange(mergeSelection(files, dropped, multiple, directory));
|
.then((dropped) => {
|
||||||
});
|
report(false, null);
|
||||||
|
if (dropped.length === 0) return;
|
||||||
|
onFilesChange(mergeSelection(files, dropped, multiple, directory));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : WALK_TIMEOUT_IT;
|
||||||
|
report(false, message);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[files, multiple, directory, onFilesChange],
|
[files, multiple, directory, onFilesChange, report],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPick = useCallback(
|
const onPick = useCallback(
|
||||||
@@ -184,28 +229,39 @@ export function FileUploadZone({
|
|||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
void pickProjectFolder().then((picked) => {
|
if (busy) return;
|
||||||
if (picked.length === 0) return;
|
report(true, null);
|
||||||
onFilesChange(picked);
|
void pickProjectFolder()
|
||||||
});
|
.then((picked) => {
|
||||||
|
report(false, null);
|
||||||
|
if (picked.length === 0) return;
|
||||||
|
onFilesChange(picked);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : WALK_TIMEOUT_IT;
|
||||||
|
report(false, message);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[onFilesChange],
|
[busy, onFilesChange, report],
|
||||||
);
|
);
|
||||||
|
|
||||||
const boxClass = cn(
|
const boxClass = cn(
|
||||||
"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 cursor-pointer transition-colors",
|
"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 cursor-pointer transition-colors",
|
||||||
hover ? "border-blue-500 bg-blue-500/5" : "border-border hover:border-foreground/20",
|
hover ? "border-blue-500 bg-blue-500/5" : "border-border hover:border-foreground/20",
|
||||||
filled && "border-emerald-500/40 bg-emerald-500/5",
|
filled && "border-emerald-500/40 bg-emerald-500/5",
|
||||||
|
busy && "pointer-events-none opacity-80",
|
||||||
);
|
);
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<>
|
<>
|
||||||
{filled ? (
|
{busy ? (
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
) : filled ? (
|
||||||
<FileCheck className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
<FileCheck className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="h-6 w-6 text-muted-foreground" />
|
<Upload className="h-6 w-6 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium">{label}</span>
|
<span className="text-sm font-medium">{busy ? "Lettura cartella…" : label}</span>
|
||||||
{preloaded && preloaded.length > 0 ? (
|
{preloaded && preloaded.length > 0 ? (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{preloaded.map((name) => (
|
{preloaded.map((name) => (
|
||||||
@@ -245,6 +301,7 @@ export function FileUploadZone({
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(boxClass, "w-full")}
|
className={cn(boxClass, "w-full")}
|
||||||
onClick={onOpenFolder}
|
onClick={onOpenFolder}
|
||||||
|
disabled={busy}
|
||||||
onDragOver={(event) => {
|
onDragOver={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setHover(true);
|
setHover(true);
|
||||||
|
|||||||
@@ -36,11 +36,31 @@ function parentDir(rel: string): string {
|
|||||||
return slash >= 0 ? rel.slice(0, slash) : "";
|
return slash >= 0 ? rel.slice(0, slash) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function skipRel(rel: string): boolean {
|
export function skipWalkDirName(name: string): boolean {
|
||||||
const low = rel.toLowerCase();
|
const n = name.replace(/\/+$/, "").toLowerCase();
|
||||||
|
if (!n) return true;
|
||||||
|
if (n.startsWith(".")) return true;
|
||||||
|
return /(-backups|\.pretty|3dmodels|__macosx|node_modules)$/i.test(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKicadProjectFileName(name: string): boolean {
|
||||||
|
const base = name.replaceAll("\\", "/").split("/").pop() || name;
|
||||||
|
const n = base.toLowerCase();
|
||||||
|
return n.endsWith(".kicad_pro") || n.endsWith(".kicad_sch") || n.endsWith(".kicad_pcb");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skipRel(rel: string): boolean {
|
||||||
|
const low = rel.replaceAll("\\", "/").toLowerCase();
|
||||||
|
const parts = low.split("/").filter(Boolean);
|
||||||
|
if (parts.some((p) => skipWalkDirName(p))) return true;
|
||||||
return low.includes("-backups/") || low.includes("/__macosx/") || low.includes(".pretty/");
|
return low.includes("-backups/") || low.includes("/__macosx/") || low.includes(".pretty/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Step 1 Next: project name + a .kicad_pro already in the pick (not waiting on the whole tree). */
|
||||||
|
export function detailsStepReady(projectName: string, files: NamedUpload[]): boolean {
|
||||||
|
return Boolean(projectName.trim() && files.some((f) => looksLikeKicadPro(f)));
|
||||||
|
}
|
||||||
|
|
||||||
function depth(rel: string): number {
|
function depth(rel: string): number {
|
||||||
const p = parentDir(rel);
|
const p = parentDir(rel);
|
||||||
if (!p) return 0;
|
if (!p) return 0;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -321,3 +322,87 @@ def test_real_hubaudio_kicad_pro_filename_and_folder_ingest(tmp_path: Path):
|
|||||||
parts, _nets, fmt = parse_netlist_any(parsed.root)
|
parts, _nets, fmt = parse_netlist_any(parsed.root)
|
||||||
assert fmt == "kicad_sch"
|
assert fmt == "kicad_sch"
|
||||||
assert "U13" not in parts
|
assert "U13" not in parts
|
||||||
|
|
||||||
|
|
||||||
|
# Mirrors periscope/src/frontend/src/lib/kicad-project-files.ts (folder walk + Next).
|
||||||
|
def _skip_walk_dir_name(name: str) -> bool:
|
||||||
|
n = name.rstrip("/").lower()
|
||||||
|
if not n or n.startswith("."):
|
||||||
|
return True
|
||||||
|
return bool(re.search(r"(-backups|\.pretty|3dmodels|__macosx|node_modules)$", n))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_kicad_project_file_name(name: str) -> bool:
|
||||||
|
base = name.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
||||||
|
return base.endswith((".kicad_pro", ".kicad_sch", ".kicad_pcb"))
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_rel(rel: str) -> bool:
|
||||||
|
parts = [p for p in rel.replace("\\", "/").lower().split("/") if p]
|
||||||
|
return any(_skip_walk_dir_name(p) for p in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _details_step_ready(project_name: str, files: list[dict[str, str]]) -> bool:
|
||||||
|
if not project_name.strip():
|
||||||
|
return False
|
||||||
|
return any(_looks_like_kicad_pro(f.get("name", ""), f.get("webkitRelativePath", "")) for f in files)
|
||||||
|
|
||||||
|
|
||||||
|
def test_folder_walk_skips_history_and_keeps_project_dir_only():
|
||||||
|
"""Live 2.60.8 recursed handle.entries() into HubAudio/.history (~2000 files) so Next never enabled."""
|
||||||
|
names = [
|
||||||
|
"HubAudio.kicad_pro",
|
||||||
|
"HubAudio.kicad_sch",
|
||||||
|
"Codec.kicad_sch",
|
||||||
|
"HubAudio.kicad_pcb",
|
||||||
|
"HubAudio.kicad_prl",
|
||||||
|
"~HubAudio.kicad_pcb.lck",
|
||||||
|
".history",
|
||||||
|
".git",
|
||||||
|
"fp-lib-table",
|
||||||
|
]
|
||||||
|
kept = [n for n in names if _is_kicad_project_file_name(n)]
|
||||||
|
skipped_dirs = [n for n in names if _skip_walk_dir_name(n)]
|
||||||
|
assert kept == [
|
||||||
|
"HubAudio.kicad_pro",
|
||||||
|
"HubAudio.kicad_sch",
|
||||||
|
"Codec.kicad_sch",
|
||||||
|
"HubAudio.kicad_pcb",
|
||||||
|
]
|
||||||
|
assert ".history" in skipped_dirs and ".git" in skipped_dirs
|
||||||
|
assert _skip_rel("HubAudio/.history/objects/aa")
|
||||||
|
assert not _skip_rel("HubAudio/HubAudio.kicad_pro")
|
||||||
|
|
||||||
|
|
||||||
|
def test_details_next_enables_when_kicad_pro_is_in():
|
||||||
|
empty: list[dict[str, str]] = []
|
||||||
|
assert not _details_step_ready("HubAudio", empty)
|
||||||
|
assert not _details_step_ready(
|
||||||
|
"",
|
||||||
|
[{"name": "HubAudio.kicad_pro", "webkitRelativePath": "HubAudio/HubAudio.kicad_pro"}],
|
||||||
|
)
|
||||||
|
assert _details_step_ready(
|
||||||
|
"HubAudio",
|
||||||
|
[{"name": "HubAudio.kicad_pro", "webkitRelativePath": "HubAudio/HubAudio.kicad_pro"}],
|
||||||
|
)
|
||||||
|
assert _details_step_ready(
|
||||||
|
"HubAudio",
|
||||||
|
[{"name": "HubAudio", "webkitRelativePath": "HubAudio/HubAudio.kicad_pro"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not (HUBAUDIO / "HubAudio.kicad_pro").is_file(), reason="HubAudio tree not on disk")
|
||||||
|
def test_real_hubaudio_recursive_tree_is_huge_project_dir_is_small():
|
||||||
|
tree = list(HUBAUDIO.rglob("*"))
|
||||||
|
project_dir = [
|
||||||
|
p
|
||||||
|
for p in HUBAUDIO.iterdir()
|
||||||
|
if p.is_file() and _is_kicad_project_file_name(p.name)
|
||||||
|
]
|
||||||
|
assert len(tree) > 100
|
||||||
|
assert (HUBAUDIO / ".history").is_dir()
|
||||||
|
assert any(p.name == "HubAudio.kicad_pro" for p in project_dir)
|
||||||
|
assert any(p.name.endswith(".kicad_sch") for p in project_dir)
|
||||||
|
assert any(p.name.endswith(".kicad_pcb") for p in project_dir)
|
||||||
|
assert all(".history" not in str(p) for p in project_dir)
|
||||||
|
assert len(project_dir) < 40
|
||||||
|
|||||||
Reference in New Issue
Block a user