Skip to content

Commit 09d051e

Browse files
vdusekclaude
andcommitted
test: add comprehensive tests for Python project detection
Test various Python project structures: - Flat package at root level - Package inside src/ container (when src is not a package) - Package with subpackages (like Scrapy - src/ with __init__.py) - Deeply nested subpackages - Multiple packages error case - No package found error case - Edge cases (hidden dirs, __pycache__, invalid identifiers) - Mixed project detection (Python + Node.js) - pyproject.toml detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 27ca7f5 commit 09d051e

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import { mkdir, rm, writeFile } from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
import { cwdCache, ProjectLanguage, useCwdProject } from '../../../src/lib/hooks/useCwdProject.js';
6+
7+
const testDir = join(fileURLToPath(import.meta.url), '..', '..', '..', 'tmp', 'useCwdProject-test');
8+
9+
async function createFile(path: string, content = '') {
10+
await mkdir(join(testDir, ...path.split('/').slice(0, -1)), { recursive: true });
11+
await writeFile(join(testDir, path), content);
12+
}
13+
14+
async function createPythonPackage(packagePath: string) {
15+
await createFile(`${packagePath}/__init__.py`);
16+
}
17+
18+
// Helper to create a requirements.txt (Python indicator)
19+
async function createRequirementsTxt() {
20+
await createFile('requirements.txt', 'apify>=1.0.0');
21+
}
22+
23+
describe('useCwdProject - Python project detection', () => {
24+
beforeEach(async () => {
25+
// Clean up test directory and cache before each test
26+
await rm(testDir, { recursive: true, force: true });
27+
await mkdir(testDir, { recursive: true });
28+
cwdCache.clear();
29+
});
30+
31+
afterAll(async () => {
32+
await rm(testDir, { recursive: true, force: true });
33+
});
34+
35+
describe('flat package structure', () => {
36+
it('should detect a flat package at root level', async () => {
37+
// Structure:
38+
// requirements.txt
39+
// my_package/
40+
// __init__.py
41+
// main.py
42+
await createRequirementsTxt();
43+
await createPythonPackage('my_package');
44+
await createFile('my_package/main.py', 'print("hello")');
45+
46+
const result = await useCwdProject({ cwd: testDir });
47+
48+
expect(result.isOk()).toBe(true);
49+
const project = result.unwrap();
50+
expect(project.type).toBe(ProjectLanguage.Python);
51+
expect(project.entrypoint?.path).toBe('my_package');
52+
});
53+
54+
it('should detect package with underscore name', async () => {
55+
// Structure:
56+
// requirements.txt
57+
// my_cool_package/
58+
// __init__.py
59+
await createRequirementsTxt();
60+
await createPythonPackage('my_cool_package');
61+
62+
const result = await useCwdProject({ cwd: testDir });
63+
64+
expect(result.isOk()).toBe(true);
65+
const project = result.unwrap();
66+
expect(project.type).toBe(ProjectLanguage.Python);
67+
expect(project.entrypoint?.path).toBe('my_cool_package');
68+
});
69+
});
70+
71+
describe('src container structure', () => {
72+
it('should detect package inside src/ when src is not a package', async () => {
73+
// Structure:
74+
// requirements.txt
75+
// src/
76+
// my_package/
77+
// __init__.py
78+
// main.py
79+
// (src/ has no __init__.py, so it's just a container)
80+
await createRequirementsTxt();
81+
await createPythonPackage('src/my_package');
82+
await createFile('src/my_package/main.py', 'print("hello")');
83+
84+
const result = await useCwdProject({ cwd: testDir });
85+
86+
expect(result.isOk()).toBe(true);
87+
const project = result.unwrap();
88+
expect(project.type).toBe(ProjectLanguage.Python);
89+
expect(project.entrypoint?.path).toBe('src.my_package');
90+
});
91+
92+
it('should detect package inside src/ with requirements.txt', async () => {
93+
// Structure:
94+
// requirements.txt
95+
// src/
96+
// my_package/
97+
// __init__.py
98+
await createRequirementsTxt();
99+
await createPythonPackage('src/my_package');
100+
101+
const result = await useCwdProject({ cwd: testDir });
102+
103+
expect(result.isOk()).toBe(true);
104+
const project = result.unwrap();
105+
expect(project.type).toBe(ProjectLanguage.Python);
106+
expect(project.entrypoint?.path).toBe('src.my_package');
107+
});
108+
});
109+
110+
describe('package with subpackages (like Scrapy)', () => {
111+
it('should detect src as the only package when it has __init__.py', async () => {
112+
// Structure (typical Scrapy template):
113+
// requirements.txt
114+
// src/
115+
// __init__.py
116+
// __main__.py
117+
// main.py
118+
// spiders/
119+
// __init__.py
120+
// my_spider.py
121+
// Here src/ IS a package, and spiders/ is a subpackage (not a separate top-level package)
122+
await createRequirementsTxt();
123+
await createPythonPackage('src');
124+
await createFile('src/__main__.py', 'from .main import main; main()');
125+
await createFile('src/main.py', 'def main(): pass');
126+
await createPythonPackage('src/spiders');
127+
await createFile('src/spiders/my_spider.py', 'class MySpider: pass');
128+
129+
const result = await useCwdProject({ cwd: testDir });
130+
131+
expect(result.isOk()).toBe(true);
132+
const project = result.unwrap();
133+
expect(project.type).toBe(ProjectLanguage.Python);
134+
// Should only find 'src', not 'src' AND 'src.spiders'
135+
expect(project.entrypoint?.path).toBe('src');
136+
});
137+
138+
it('should detect package with deeply nested subpackages', async () => {
139+
// Structure:
140+
// requirements.txt
141+
// my_app/
142+
// __init__.py
143+
// core/
144+
// __init__.py
145+
// utils/
146+
// __init__.py
147+
// helpers/
148+
// __init__.py
149+
await createRequirementsTxt();
150+
await createPythonPackage('my_app');
151+
await createPythonPackage('my_app/core');
152+
await createPythonPackage('my_app/utils');
153+
await createPythonPackage('my_app/utils/helpers');
154+
155+
const result = await useCwdProject({ cwd: testDir });
156+
157+
expect(result.isOk()).toBe(true);
158+
const project = result.unwrap();
159+
expect(project.type).toBe(ProjectLanguage.Python);
160+
// Should only find 'my_app' as the top-level package
161+
expect(project.entrypoint?.path).toBe('my_app');
162+
});
163+
});
164+
165+
describe('multiple packages (error case)', () => {
166+
it('should error when multiple top-level packages exist at root', async () => {
167+
// Structure:
168+
// requirements.txt
169+
// package_a/
170+
// __init__.py
171+
// package_b/
172+
// __init__.py
173+
await createRequirementsTxt();
174+
await createPythonPackage('package_a');
175+
await createPythonPackage('package_b');
176+
177+
const result = await useCwdProject({ cwd: testDir });
178+
179+
expect(result.isErr()).toBe(true);
180+
const error = result.unwrapErr();
181+
expect(error.message).toContain('Multiple Python packages found');
182+
expect(error.message).toContain('package_a');
183+
expect(error.message).toContain('package_b');
184+
});
185+
186+
it('should error when multiple packages exist in src/ container', async () => {
187+
// Structure:
188+
// requirements.txt
189+
// src/
190+
// package_a/
191+
// __init__.py
192+
// package_b/
193+
// __init__.py
194+
// (src/ has no __init__.py)
195+
await createRequirementsTxt();
196+
await createPythonPackage('src/package_a');
197+
await createPythonPackage('src/package_b');
198+
199+
const result = await useCwdProject({ cwd: testDir });
200+
201+
expect(result.isErr()).toBe(true);
202+
const error = result.unwrapErr();
203+
expect(error.message).toContain('Multiple Python packages found');
204+
});
205+
});
206+
207+
describe('no package found (error case)', () => {
208+
it('should error when Python files exist but no valid package structure', async () => {
209+
// Structure:
210+
// main.py (no package, just loose files)
211+
// requirements.txt
212+
await createFile('requirements.txt', 'apify>=1.0.0');
213+
await createFile('main.py', 'print("hello")');
214+
215+
const result = await useCwdProject({ cwd: testDir });
216+
217+
expect(result.isErr()).toBe(true);
218+
const error = result.unwrapErr();
219+
expect(error.message).toContain('No Python package found');
220+
});
221+
222+
it('should error when directory exists but has no __init__.py', async () => {
223+
// Structure:
224+
// main.py (root level Python file - triggers Python detection)
225+
// my_package/
226+
// other.py (no __init__.py in my_package!)
227+
// requirements.txt
228+
await createFile('requirements.txt', 'apify>=1.0.0');
229+
await createFile('main.py', 'print("root")'); // Python file in root triggers detection
230+
await mkdir(join(testDir, 'my_package'), { recursive: true });
231+
await createFile('my_package/other.py', 'print("hello")');
232+
233+
const result = await useCwdProject({ cwd: testDir });
234+
235+
expect(result.isErr()).toBe(true);
236+
const error = result.unwrapErr();
237+
// Should detect Python files but no valid package structure
238+
expect(error.message).toContain('No Python package found');
239+
expect(error.message).toContain('no valid package structure');
240+
});
241+
});
242+
243+
describe('edge cases', () => {
244+
it('should ignore hidden directories', async () => {
245+
// Structure:
246+
// requirements.txt
247+
// .venv/
248+
// __init__.py (should be ignored)
249+
// my_package/
250+
// __init__.py
251+
await createRequirementsTxt();
252+
await createPythonPackage('.venv');
253+
await createPythonPackage('my_package');
254+
255+
const result = await useCwdProject({ cwd: testDir });
256+
257+
expect(result.isOk()).toBe(true);
258+
const project = result.unwrap();
259+
expect(project.entrypoint?.path).toBe('my_package');
260+
});
261+
262+
it('should ignore directories starting with underscore', async () => {
263+
// Structure:
264+
// requirements.txt
265+
// __pycache__/
266+
// __init__.py (should be ignored)
267+
// my_package/
268+
// __init__.py
269+
await createRequirementsTxt();
270+
await createPythonPackage('__pycache__');
271+
await createPythonPackage('my_package');
272+
273+
const result = await useCwdProject({ cwd: testDir });
274+
275+
expect(result.isOk()).toBe(true);
276+
const project = result.unwrap();
277+
expect(project.entrypoint?.path).toBe('my_package');
278+
});
279+
280+
it('should ignore directories with invalid Python identifier names', async () => {
281+
// Structure:
282+
// requirements.txt
283+
// my-package/ (hyphen is invalid in Python identifiers)
284+
// __init__.py
285+
// my_package/
286+
// __init__.py
287+
await createRequirementsTxt();
288+
await createPythonPackage('my-package');
289+
await createPythonPackage('my_package');
290+
291+
const result = await useCwdProject({ cwd: testDir });
292+
293+
expect(result.isOk()).toBe(true);
294+
const project = result.unwrap();
295+
// Should only find my_package, not my-package
296+
expect(project.entrypoint?.path).toBe('my_package');
297+
});
298+
299+
it('should detect project with pyproject.toml', async () => {
300+
// Structure:
301+
// pyproject.toml
302+
// my_package/
303+
// __init__.py
304+
await createFile('pyproject.toml', '[project]\nname = "my-package"');
305+
await createPythonPackage('my_package');
306+
307+
const result = await useCwdProject({ cwd: testDir });
308+
309+
expect(result.isOk()).toBe(true);
310+
const project = result.unwrap();
311+
expect(project.type).toBe(ProjectLanguage.Python);
312+
expect(project.entrypoint?.path).toBe('my_package');
313+
});
314+
315+
it('should not detect Python when no indicators present', async () => {
316+
// Structure:
317+
// Empty directory or non-Python files only
318+
await createFile('readme.txt', 'Hello');
319+
320+
const result = await useCwdProject({ cwd: testDir });
321+
322+
expect(result.isOk()).toBe(true);
323+
const project = result.unwrap();
324+
expect(project.type).toBe(ProjectLanguage.Unknown);
325+
});
326+
});
327+
328+
describe('mixed project detection', () => {
329+
it('should error when both Python and Node.js indicators are present', async () => {
330+
// Structure:
331+
// package.json
332+
// requirements.txt
333+
// my_package/
334+
// __init__.py
335+
await createFile('package.json', '{"name": "test"}');
336+
await createFile('requirements.txt', 'apify>=1.0.0');
337+
await createPythonPackage('my_package');
338+
339+
const result = await useCwdProject({ cwd: testDir });
340+
341+
expect(result.isErr()).toBe(true);
342+
const error = result.unwrapErr();
343+
expect(error.message).toContain('Mixed project detected');
344+
});
345+
});
346+
});

0 commit comments

Comments
 (0)