diff --git a/jest.config.js b/jest.config.js index a01e9ce..04ed858 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,21 +8,14 @@ const baseConfig = { coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html', 'json'], - // On Windows CI several CLI/integration suites are skipped (see - // tests/cli-*.test.ts TODO(windows-ci) markers), which would otherwise - // drop coverage below thresholds and mask the real signal. Skip the - // coverage gate on win32. - coverageThreshold: - process.platform === 'win32' - ? undefined - : { - global: { - branches: 55, - functions: 75, - lines: 68, - statements: 68, - }, - }, + coverageThreshold: { + global: { + branches: 55, + functions: 75, + lines: 68, + statements: 68, + }, + }, testTimeout: 10000, verbose: true, maxWorkers: 1, diff --git a/tests/cli-program.test.ts b/tests/cli-program.test.ts index e629839..069fc0a 100644 --- a/tests/cli-program.test.ts +++ b/tests/cli-program.test.ts @@ -35,8 +35,8 @@ jest.mock('chokidar', () => { import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; import * as cliProgram from '../src/cli/program'; +import { canonicalTmpDir } from './helpers/paths'; const { createProgram, compileFile } = cliProgram; @@ -47,9 +47,7 @@ const { createProgram, compileFile } = cliProgram; * We stub process.exit and console to keep the test runner alive and to assert outputs. */ -// TODO(windows-ci): this suite uses Windows 8.3 short paths and npm init in a temp -// directory; it needs explicit long-path handling before it can pass on win32. -(process.platform === 'win32' ? describe.skip : describe)('CLI Program (in-process)', () => { +describe('CLI Program (in-process)', () => { let tempDir: string; let originalCwd: string; let originalExitCode: number | undefined; @@ -59,7 +57,7 @@ const { createProgram, compileFile } = cliProgram; let skipCleanup = false; beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'somon-cli-program-')); + tempDir = canonicalTmpDir('somon-cli-program-'); originalCwd = process.cwd(); originalExitCode = process.exitCode; consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); @@ -72,13 +70,27 @@ const { createProgram, compileFile } = cliProgram; // Config loader behavior is covered in tests/config.test.ts. afterEach(() => { + // Restore cwd FIRST — on Windows, rmSync on the current working + // directory raises EBUSY, which would throw and leave the next suite + // with a stale cwd pointing at a deleted temp dir. + try { + process.chdir(originalCwd); + } catch { + // originalCwd may itself be gone in pathological cases; swallow and + // keep going so we still restore spies and exit code. + } + // Cleanup temp dir if (!skipCleanup && fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch { + // Windows occasionally keeps file handles open briefly after a + // subprocess exits; the OS will reclaim the temp dir, and a failed + // cleanup here must not break subsequent suites. + } } - // Restore cwd - process.chdir(originalCwd); // Reset any exit code left by CLI handlers during tests process.exitCode = originalExitCode ?? 0; consoleLogSpy.mockRestore(); diff --git a/tests/cli-run-modules.test.ts b/tests/cli-run-modules.test.ts index 12f7597..708f353 100644 --- a/tests/cli-run-modules.test.ts +++ b/tests/cli-run-modules.test.ts @@ -1,88 +1,81 @@ -import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; - -// TODO(windows-ci): module-resolver normalises paths with POSIX separators; the -// assertions here compare against absolute paths that use '\' on Windows. -(process.platform === 'win32' ? describe.skip : describe)( - 'CLI Run Command - Module Imports', - () => { - let tempDir: string; - let cliPath: string; - - beforeAll(() => { - // Build the project first - execSync('npm run build', { stdio: 'pipe' }); - cliPath = path.join(__dirname, '..', 'dist', 'cli.js'); - }); - - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'somon-run-modules-test-')); - }); - - afterEach(() => { - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - - describe('basic module imports', () => { - test('should run file with single module import', () => { - // Create a utility module - const utilsFile = path.join(tempDir, 'utils.som'); - fs.writeFileSync( - utilsFile, - `содир функсия салом(ном: сатр): сатр { +import { buildCliOnce, canonicalTmpDir, runCli } from './helpers/paths'; + +describe('CLI Run Command - Module Imports', () => { + let tempDir: string; + // cliPath is resolved via buildCliOnce but not needed directly anymore — + // runCli resolves it internally. Kept as a variable to minimise diff. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let cliPath: string; + + beforeAll(() => { + cliPath = buildCliOnce(); + }); + + beforeEach(() => { + tempDir = canonicalTmpDir('somon-run-modules-test-'); + }); + + afterEach(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe('basic module imports', () => { + test('should run file with single module import', () => { + // Create a utility module + const utilsFile = path.join(tempDir, 'utils.som'); + fs.writeFileSync( + utilsFile, + `содир функсия салом(ном: сатр): сатр { бозгашт "Салом, " + ном + "!"; }` - ); + ); - // Create main file that imports the utility - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { салом } аз "./utils"; + // Create main file that imports the utility + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { салом } аз "./utils"; чоп.сабт(салом("Ҷаҳон"));` - ); - - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); - - expect(result).toContain('Салом, Ҷаҳон!'); - }); - - test('should run file with multiple module imports', () => { - // Create math module - const mathFile = path.join(tempDir, 'math.som'); - fs.writeFileSync( - mathFile, - `содир функсия ҷамъ(а: рақам, б: рақам): рақам { + ); + + const result = runCli(['run', mainFile], { cwd: tempDir }); + + expect(result).toContain('Салом, Ҷаҳон!'); + }); + + test('should run file with multiple module imports', () => { + // Create math module + const mathFile = path.join(tempDir, 'math.som'); + fs.writeFileSync( + mathFile, + `содир функсия ҷамъ(а: рақам, б: рақам): рақам { бозгашт а + б; } содир функсия зарб(а: рақам, б: рақам): рақам { бозгашт а * б; }` - ); + ); - // Create string module - const stringFile = path.join(tempDir, 'string.som'); - fs.writeFileSync( - stringFile, - `содир функсия калон(матн: сатр): сатр { + // Create string module + const stringFile = path.join(tempDir, 'string.som'); + fs.writeFileSync( + stringFile, + `содир функсия калон(матн: сатр): сатр { бозгашт матн.toUpperCase(); }` - ); + ); - // Create main file that imports from both modules - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { ҷамъ, зарб } аз "./math"; + // Create main file that imports from both modules + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { ҷамъ, зарб } аз "./math"; ворид { калон } аз "./string"; тағйирёбанда натиҷа = ҷамъ(5, 3); @@ -92,181 +85,163 @@ import * as os from 'os'; чоп.сабт("4 × 7 = " + натиҷа2); чоп.сабт(калон("тест"));` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + const result = runCli(['run', mainFile], { cwd: tempDir }); - expect(result).toContain('5 + 3 = 8'); - expect(result).toContain('4 × 7 = 28'); - expect(result).toContain('ТЕСТ'); - }); + expect(result).toContain('5 + 3 = 8'); + expect(result).toContain('4 × 7 = 28'); + expect(result).toContain('ТЕСТ'); + }); - test('should run file with nested module imports', () => { - // Create base module - const baseFile = path.join(tempDir, 'base.som'); - fs.writeFileSync( - baseFile, - `содир собит ПИ: рақам = 3.14159; + test('should run file with nested module imports', () => { + // Create base module + const baseFile = path.join(tempDir, 'base.som'); + fs.writeFileSync( + baseFile, + `содир собит ПИ: рақам = 3.14159; содир функсия квадрат(х: рақам): рақам { бозгашт х * х; }` - ); + ); - // Create derived module that imports base - const derivedFile = path.join(tempDir, 'derived.som'); - fs.writeFileSync( - derivedFile, - `ворид { ПИ, квадрат } аз "./base"; + // Create derived module that imports base + const derivedFile = path.join(tempDir, 'derived.som'); + fs.writeFileSync( + derivedFile, + `ворид { ПИ, квадрат } аз "./base"; содир функсия масоҳати_доира(р: рақам): рақам { бозгашт ПИ * квадрат(р); }` - ); + ); - // Create main file that imports derived - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { масоҳати_доира } аз "./derived"; + // Create main file that imports derived + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { масоҳати_доира } аз "./derived"; тағйирёбанда масоҳат = масоҳати_доира(10); чоп.сабт("Масоҳат: " + масоҳат);` - ); - - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); - - expect(result).toContain('Масоҳат:'); - expect(result).toMatch(/314\.159/); // Should calculate area correctly - }); - - test('should run file with default export import', () => { - // Create module with default export - const moduleFile = path.join(tempDir, 'module.som'); - fs.writeFileSync( - moduleFile, - `содир пешфарз функсия асосӣ(): сатр { + ); + + const result = runCli(['run', mainFile], { cwd: tempDir }); + + expect(result).toContain('Масоҳат:'); + expect(result).toMatch(/314\.159/); // Should calculate area correctly + }); + + test('should run file with default export import', () => { + // Create module with default export + const moduleFile = path.join(tempDir, 'module.som'); + fs.writeFileSync( + moduleFile, + `содир пешфарз функсия асосӣ(): сатр { бозгашт "Функсияи пешфарз"; }` - ); + ); - // Create main file that imports default - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид асосӣ аз "./module"; + // Create main file that imports default + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид асосӣ аз "./module"; чоп.сабт(асосӣ());` - ); - - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); - - expect(result).toContain('Функсияи пешфарз'); - }); - - test('should run file with aliased imports', () => { - // Create module - const moduleFile = path.join(tempDir, 'module.som'); - fs.writeFileSync( - moduleFile, - `содир функсия функсия_бо_номи_дароз(х: рақам): рақам { + ); + + const result = runCli(['run', mainFile], { cwd: tempDir }); + + expect(result).toContain('Функсияи пешфарз'); + }); + + test('should run file with aliased imports', () => { + // Create module + const moduleFile = path.join(tempDir, 'module.som'); + fs.writeFileSync( + moduleFile, + `содир функсия функсия_бо_номи_дароз(х: рақам): рақам { бозгашт х * 2; }` - ); + ); - // Create main file with aliased import - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { функсия_бо_номи_дароз чун дубаракуни } аз "./module"; + // Create main file with aliased import + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { функсия_бо_номи_дароз чун дубаракуни } аз "./module"; чоп.сабт(дубаракуни(5));` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + const result = runCli(['run', mainFile], { cwd: tempDir }); - expect(result).toContain('10'); - }); + expect(result).toContain('10'); }); - - describe('module imports with subdirectories', () => { - test('should run file with imports from subdirectory', () => { - // Create subdirectory - const libDir = path.join(tempDir, 'lib'); - fs.mkdirSync(libDir); - - // Create module in subdirectory - const moduleFile = path.join(libDir, 'utils.som'); - fs.writeFileSync( - moduleFile, - `содир функсия ҳисоб(х: рақам): рақам { + }); + + describe('module imports with subdirectories', () => { + test('should run file with imports from subdirectory', () => { + // Create subdirectory + const libDir = path.join(tempDir, 'lib'); + fs.mkdirSync(libDir); + + // Create module in subdirectory + const moduleFile = path.join(libDir, 'utils.som'); + fs.writeFileSync( + moduleFile, + `содир функсия ҳисоб(х: рақам): рақам { бозгашт х + 10; }` - ); + ); - // Create main file - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { ҳисоб } аз "./lib/utils"; + // Create main file + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { ҳисоб } аз "./lib/utils"; чоп.сабт(ҳисоб(5));` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + const result = runCli(['run', mainFile], { cwd: tempDir }); - expect(result).toContain('15'); - }); + expect(result).toContain('15'); + }); - test('should run file with imports from parent directory', () => { - // Create module in temp root - const moduleFile = path.join(tempDir, 'shared.som'); - fs.writeFileSync(moduleFile, `содир собит РАҚАМ: рақам = 42;`); + test('should run file with imports from parent directory', () => { + // Create module in temp root + const moduleFile = path.join(tempDir, 'shared.som'); + fs.writeFileSync(moduleFile, `содир собит РАҚАМ: рақам = 42;`); - // Create subdirectory - const subDir = path.join(tempDir, 'sub'); - fs.mkdirSync(subDir); + // Create subdirectory + const subDir = path.join(tempDir, 'sub'); + fs.mkdirSync(subDir); - // Create main file in subdirectory - const mainFile = path.join(subDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { РАҚАМ } аз "../shared"; + // Create main file in subdirectory + const mainFile = path.join(subDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { РАҚАМ } аз "../shared"; чоп.сабт("РАҚАМ: " + РАҚАМ);` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: subDir, - }); + const result = runCli(['run', mainFile], { cwd: subDir }); - expect(result).toContain('РАҚАМ: 42'); - }); + expect(result).toContain('РАҚАМ: 42'); }); - - describe('module imports with classes', () => { - test('should run file with imported class', () => { - // Create class module - const classFile = path.join(tempDir, 'counter.som'); - fs.writeFileSync( - classFile, - `содир синф Ҳисобгар { + }); + + describe('module imports with classes', () => { + test('should run file with imported class', () => { + // Create class module + const classFile = path.join(tempDir, 'counter.som'); + fs.writeFileSync( + classFile, + `содир синф Ҳисобгар { хосусӣ шумора: рақам; конструктор() { @@ -281,239 +256,214 @@ import * as os from 'os'; бозгашт ин.шумора; } }` - ); + ); - // Create main file that uses the class - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { Ҳисобгар } аз "./counter"; + // Create main file that uses the class + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { Ҳисобгар } аз "./counter"; тағйирёбанда ҳисобгар = нав Ҳисобгар(); ҳисобгар.афзоиш(); ҳисобгар.афзоиш(); ҳисобгар.афзоиш(); чоп.сабт("Шумора: " + ҳисобгар.гирифтан());` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + const result = runCli(['run', mainFile], { cwd: tempDir }); - expect(result).toContain('Шумора: 3'); - }); + expect(result).toContain('Шумора: 3'); }); + }); - describe('error handling with modules', () => { - test('should handle missing module gracefully', () => { - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { функсия } аз "./nonexistent"; + describe('error handling with modules', () => { + test('should handle missing module gracefully', () => { + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { функсия } аз "./nonexistent"; чоп.сабт("Test");` - ); - - expect(() => { - execSync(`node "${cliPath}" run "${mainFile}"`, { - stdio: 'pipe', - cwd: tempDir, - }); - }).toThrow(); - }); - - test('should handle module with syntax errors', () => { - // Create module with syntax error - const moduleFile = path.join(tempDir, 'broken.som'); - fs.writeFileSync(moduleFile, 'invalid syntax here!!!'); - - // Create main file that tries to import it - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { функсия } аз "./broken"; + ); + + expect(() => { + runCli(['run', mainFile], { cwd: tempDir, stdio: 'pipe' }); + }).toThrow(); + }); + + test('should handle module with syntax errors', () => { + // Create module with syntax error + const moduleFile = path.join(tempDir, 'broken.som'); + fs.writeFileSync(moduleFile, 'invalid syntax here!!!'); + + // Create main file that tries to import it + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { функсия } аз "./broken"; чоп.сабт("Test");` - ); - - expect(() => { - execSync(`node "${cliPath}" run "${mainFile}"`, { - stdio: 'pipe', - cwd: tempDir, - }); - }).toThrow(); - }); - - test('should handle circular dependencies', () => { - // Create module A that imports B - const moduleA = path.join(tempDir, 'a.som'); - fs.writeFileSync( - moduleA, - `ворид { функсияБ } аз "./b"; + ); + + expect(() => { + runCli(['run', mainFile], { cwd: tempDir, stdio: 'pipe' }); + }).toThrow(); + }); + + test('should handle circular dependencies', () => { + // Create module A that imports B + const moduleA = path.join(tempDir, 'a.som'); + fs.writeFileSync( + moduleA, + `ворид { функсияБ } аз "./b"; содир функсия функсияА(): сатр { бозгашт "A calls " + функсияБ(); }` - ); + ); - // Create module B that imports A (circular) - const moduleB = path.join(tempDir, 'b.som'); - fs.writeFileSync( - moduleB, - `ворид { функсияА } аз "./a"; + // Create module B that imports A (circular) + const moduleB = path.join(tempDir, 'b.som'); + fs.writeFileSync( + moduleB, + `ворид { функсияА } аз "./a"; содир функсия функсияБ(): сатр { бозгашт "B"; }` - ); + ); - // Create main file - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { функсияА } аз "./a"; + // Create main file + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { функсияА } аз "./a"; чоп.сабт(функсияА());` - ); - - // Should detect and report circular dependency - expect(() => { - execSync(`node "${cliPath}" run "${mainFile}"`, { - stdio: 'pipe', - cwd: tempDir, - }); - }).toThrow(); - }); - }); + ); - describe('module imports with constants and variables', () => { - test('should run file with imported constants', () => { - // Create constants module - const constantsFile = path.join(tempDir, 'constants.som'); - fs.writeFileSync( - constantsFile, - `содир собит МАКСИМУМ: рақам = 100; + // Should detect and report circular dependency + expect(() => { + runCli(['run', mainFile], { cwd: tempDir, stdio: 'pipe' }); + }).toThrow(); + }); + }); + + describe('module imports with constants and variables', () => { + test('should run file with imported constants', () => { + // Create constants module + const constantsFile = path.join(tempDir, 'constants.som'); + fs.writeFileSync( + constantsFile, + `содир собит МАКСИМУМ: рақам = 100; содир собит МИНИМУМ: рақам = 0; содир собит НОМ: сатр = "СомонСкрипт";` - ); + ); - // Create main file - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { МАКСИМУМ, МИНИМУМ, НОМ } аз "./constants"; + // Create main file + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { МАКСИМУМ, МИНИМУМ, НОМ } аз "./constants"; чоп.сабт("Барнома: " + НОМ); чоп.сабт("Диапазон: " + МИНИМУМ + " - " + МАКСИМУМ);` - ); + ); - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + const result = runCli(['run', mainFile], { cwd: tempDir }); - expect(result).toContain('СомонСкрипт'); - expect(result).toContain('0 - 100'); - }); + expect(result).toContain('СомонСкрипт'); + expect(result).toContain('0 - 100'); }); + }); - describe('real-world example', () => { - test('should run the 37-module-imports-demo example', () => { - const examplePath = path.join(__dirname, '..', 'examples', '37-module-imports-demo'); - const mainFile = path.join(examplePath, 'main.som'); - - // Check if the example exists - if (!fs.existsSync(mainFile)) { - console.warn('Skipping real-world example test - example not found'); - return; - } - - const result = execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: examplePath, - }); - - // Verify key outputs from the example - expect(result).toContain('Модули математикӣ'); - expect(result).toContain('Модули сатрӣ'); - expect(result).toContain('ПИ'); - expect(result).toContain('Модулҳо бомуваффақият ворид шуданд'); - }); - }); + describe('real-world example', () => { + test('should run the 37-module-imports-demo example', () => { + const examplePath = path.join(__dirname, '..', 'examples', '37-module-imports-demo'); + const mainFile = path.join(examplePath, 'main.som'); - describe('performance and cleanup', () => { - test('should clean up temporary files after execution', () => { - // Create a simple module - const moduleFile = path.join(tempDir, 'module.som'); - fs.writeFileSync( - moduleFile, - `содир функсия тест(): сатр { + // Check if the example exists + if (!fs.existsSync(mainFile)) { + console.warn('Skipping real-world example test - example not found'); + return; + } + + const result = runCli(['run', mainFile], { cwd: examplePath }); + + // Verify key outputs from the example + expect(result).toContain('Модули математикӣ'); + expect(result).toContain('Модули сатрӣ'); + expect(result).toContain('ПИ'); + expect(result).toContain('Модулҳо бомуваффақият ворид шуданд'); + }); + }); + + describe('performance and cleanup', () => { + test('should clean up temporary files after execution', () => { + // Create a simple module + const moduleFile = path.join(tempDir, 'module.som'); + fs.writeFileSync( + moduleFile, + `содир функсия тест(): сатр { бозгашт "test"; }` - ); + ); - // Create main file - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { тест } аз "./module"; + // Create main file + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { тест } аз "./module"; чоп.сабт(тест());` - ); - - // Get list of files before running - const filesBefore = fs.readdirSync(tempDir); - - // Run the command - execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); - - // Get list of files after running - const filesAfter = fs.readdirSync(tempDir); - - // Should not have any extra temporary .js files left over - const tempJsFiles = filesAfter.filter(f => f.includes('.somon-run-') && f.endsWith('.js')); - expect(tempJsFiles.length).toBe(0); - - // Should only have the original .som files - expect(filesAfter.length).toBe(filesBefore.length); - }); - - test('should execute bundled code quickly', () => { - // Create a simple module - const moduleFile = path.join(tempDir, 'fast.som'); - fs.writeFileSync( - moduleFile, - `содир функсия тез(): сатр { + ); + + // Get list of files before running + const filesBefore = fs.readdirSync(tempDir); + + // Run the command + runCli(['run', mainFile], { cwd: tempDir }); + + // Get list of files after running + const filesAfter = fs.readdirSync(tempDir); + + // Should not have any extra temporary .js files left over + const tempJsFiles = filesAfter.filter(f => f.includes('.somon-run-') && f.endsWith('.js')); + expect(tempJsFiles.length).toBe(0); + + // Should only have the original .som files + expect(filesAfter.length).toBe(filesBefore.length); + }); + + test('should execute bundled code quickly', () => { + // Create a simple module + const moduleFile = path.join(tempDir, 'fast.som'); + fs.writeFileSync( + moduleFile, + `содир функсия тез(): сатр { бозгашт "Fast execution"; }` - ); + ); - // Create main file - const mainFile = path.join(tempDir, 'main.som'); - fs.writeFileSync( - mainFile, - `ворид { тез } аз "./fast"; + // Create main file + const mainFile = path.join(tempDir, 'main.som'); + fs.writeFileSync( + mainFile, + `ворид { тез } аз "./fast"; чоп.сабт(тез());` - ); + ); - const startTime = Date.now(); + const startTime = Date.now(); - execSync(`node "${cliPath}" run "${mainFile}"`, { - encoding: 'utf-8', - cwd: tempDir, - }); + runCli(['run', mainFile], { cwd: tempDir }); - const endTime = Date.now(); - const executionTime = endTime - startTime; + const endTime = Date.now(); + const executionTime = endTime - startTime; - // Should execute in reasonable time (less than 5 seconds) - expect(executionTime).toBeLessThan(5000); - }); + // Should execute in reasonable time (less than 5 seconds) + expect(executionTime).toBeLessThan(5000); }); - } -); + }); +}); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index c502614..c258530 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,22 +1,16 @@ -import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; +import { buildCliOnce, canonicalTmpDir, runCli } from './helpers/paths'; -// TODO(windows-ci): uses execSync + temp dirs with Windows 8.3 short names; -// several assertions compare stderr text that differs between platforms. -(process.platform === 'win32' ? describe.skip : describe)('CLI Integration Tests', () => { +describe('CLI Integration Tests', () => { let tempDir: string; - let cliPath: string; beforeAll(() => { - // Build the project first - execSync('npm run build', { stdio: 'pipe' }); - cliPath = path.join(__dirname, '..', 'dist', 'cli.js'); + buildCliOnce(); }); beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'somon-test-')); + tempDir = canonicalTmpDir('somon-test-'); }); afterEach(() => { @@ -32,9 +26,7 @@ import * as os from 'os'; fs.writeFileSync(inputFile, 'чоп.сабт("Салом ҷаҳон!");'); - const result = execSync(`node "${cliPath}" compile "${inputFile}" -o "${outputFile}"`, { - encoding: 'utf-8', - }); + const result = runCli(['compile', inputFile, '-o', outputFile]); expect(fs.existsSync(outputFile)).toBe(true); expect(result).toContain('Compiled'); @@ -49,11 +41,9 @@ import * as os from 'os'; fs.writeFileSync(inputFile, 'invalid syntax here'); try { - execSync(`node "${cliPath}" compile "${inputFile}"`, { stdio: 'pipe' }); - // If we reach here, the command succeeded when it should have failed + runCli(['compile', inputFile], { stdio: 'pipe' }); throw new Error('Expected compilation to fail but it succeeded'); } catch (error: any) { - // Expect the command to exit with non-zero status expect(error.status).not.toBe(0); } }); @@ -62,7 +52,7 @@ import * as os from 'os'; const nonExistentFile = path.join(tempDir, 'nonexistent.som'); expect(() => { - execSync(`node "${cliPath}" compile "${nonExistentFile}"`, { stdio: 'pipe' }); + runCli(['compile', nonExistentFile], { stdio: 'pipe' }); }).toThrow(); }); @@ -70,9 +60,7 @@ import * as os from 'os'; const inputFile = path.join(tempDir, 'typed.som'); fs.writeFileSync(inputFile, 'тағйирёбанда ном: сатр = "Аҳмад";'); - const result = execSync(`node "${cliPath}" compile "${inputFile}" --strict`, { - encoding: 'utf-8', - }); + const result = runCli(['compile', inputFile, '--strict']); expect(result).toContain('Compiled'); }); @@ -83,9 +71,7 @@ import * as os from 'os'; fs.writeFileSync(inputFile, 'чоп.сабт("Test");'); - execSync(`node "${cliPath}" compile "${inputFile}" -o "${outputFile}" --source-map`, { - stdio: 'pipe', - }); + runCli(['compile', inputFile, '-o', outputFile, '--source-map'], { stdio: 'pipe' }); expect(fs.existsSync(outputFile)).toBe(true); // Note: Source map generation may not be fully implemented yet @@ -98,9 +84,7 @@ import * as os from 'os'; const inputFile = path.join(tempDir, 'run-test.som'); fs.writeFileSync(inputFile, 'чоп.сабт("Running test");'); - const result = execSync(`node "${cliPath}" run "${inputFile}"`, { - encoding: 'utf-8', - }); + const result = runCli(['run', inputFile]); expect(result).toContain('Running test'); }); @@ -110,7 +94,7 @@ import * as os from 'os'; fs.writeFileSync(inputFile, 'invalid_function_call();'); expect(() => { - execSync(`node "${cliPath}" run "${inputFile}"`, { stdio: 'pipe' }); + runCli(['run', inputFile], { stdio: 'pipe' }); }).toThrow(); }); }); @@ -120,10 +104,7 @@ import * as os from 'os'; const projectName = 'test-project'; const projectPath = path.join(tempDir, projectName); - const result = execSync(`node "${cliPath}" init "${projectName}"`, { - cwd: tempDir, - encoding: 'utf-8', - }); + const result = runCli(['init', projectName], { cwd: tempDir }); expect(result).toContain('Created SomonScript project'); expect(fs.existsSync(projectPath)).toBe(true); @@ -145,18 +126,12 @@ import * as os from 'os'; fs.mkdirSync(projectPath); expect(() => { - execSync(`node "${cliPath}" init "${projectName}"`, { - cwd: tempDir, - stdio: 'pipe', - }); + runCli(['init', projectName], { cwd: tempDir, stdio: 'pipe' }); }).toThrow(); }); test('should use default project name', () => { - const result = execSync(`node "${cliPath}" init`, { - cwd: tempDir, - encoding: 'utf-8', - }); + const result = runCli(['init'], { cwd: tempDir }); expect(result).toContain('somon-project'); expect(fs.existsSync(path.join(tempDir, 'somon-project'))).toBe(true); @@ -165,12 +140,12 @@ import * as os from 'os'; describe('version and help', () => { test('should display version', () => { - const result = execSync(`node "${cliPath}" --version`, { encoding: 'utf-8' }); + const result = runCli(['--version']); expect(result).toMatch(/\d+\.\d+\.\d+/); }); test('should display help', () => { - const result = execSync(`node "${cliPath}" --help`, { encoding: 'utf-8' }); + const result = runCli(['--help']); expect(result).toContain('SomonScript compiler'); expect(result).toContain('compile'); expect(result).toContain('run'); diff --git a/tests/examples.test.ts b/tests/examples.test.ts index de4d13e..1eac71a 100644 --- a/tests/examples.test.ts +++ b/tests/examples.test.ts @@ -2,444 +2,443 @@ import { compile } from '../src/compiler'; import * as fs from 'fs'; import * as path from 'path'; -// TODO(windows-ci): spawns the compiled CLI as a child process with cwd pointing -// at a temp dir; path handling and shell invocation diverge on win32. -(process.platform === 'win32' ? describe.skip : describe)( - 'SomonScript Examples - Comprehensive Tests', - () => { - const examplesDir = path.join(__dirname, '..', 'examples'); - const tempDir = path.join(__dirname, '..', 'dist', 'temp-examples-test'); - - // Ensure temp directory exists - beforeAll(() => { - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - }); - - // Clean up temp directory after tests - afterAll(() => { - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - - // Helper function to get all .som files recursively - function getAllSomFiles(dir: string, basePath: string = ''): string[] { - const files: string[] = []; - const entries = fs.readdirSync(dir); - - for (const entry of entries) { - const fullPath = path.join(dir, entry); - const relativePath = path.join(basePath, entry); - const stat = fs.statSync(fullPath); +describe('SomonScript Examples - Comprehensive Tests', () => { + const examplesDir = path.join(__dirname, '..', 'examples'); + const tempDir = path.join(__dirname, '..', 'dist', 'temp-examples-test'); + + // Ensure temp directory exists + beforeAll(() => { + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + }); - if (stat.isDirectory()) { - files.push(...getAllSomFiles(fullPath, relativePath)); - } else if (entry.endsWith('.som')) { - files.push(relativePath); - } + // Clean up temp directory after tests + afterAll(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + // Helper function to get all .som files recursively + function getAllSomFiles(dir: string, basePath: string = ''): string[] { + const files: string[] = []; + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const relativePath = path.join(basePath, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + files.push(...getAllSomFiles(fullPath, relativePath)); + } else if (entry.endsWith('.som')) { + files.push(relativePath); } - - return files.sort(); } - // Get all example files - const exampleFiles = getAllSomFiles(examplesDir); - - // Define categories of examples - const basicExamples = exampleFiles.filter( - f => /^0[1-9]-/.test(path.basename(f)) || path.basename(f).startsWith('simple') - ); + return files.sort(); + } - const advancedExamples = exampleFiles.filter(f => /^[12][0-9]-/.test(path.basename(f))); + // Get all example files + const exampleFiles = getAllSomFiles(examplesDir); + + // Define categories of examples + const basicExamples = exampleFiles.filter( + f => /^0[1-9]-/.test(path.basename(f)) || path.basename(f).startsWith('simple') + ); + + const advancedExamples = exampleFiles.filter(f => /^[12][0-9]-/.test(path.basename(f))); + + // `path.join` on Windows emits '\\' as the separator, so a bare '/' match + // would produce an empty array on win32 and trip test.each. + const moduleExamples = exampleFiles.filter( + f => f.includes(`modules${path.sep}`) || f.includes('modules/') + ); + + const testExamples = exampleFiles.filter( + f => path.basename(f).startsWith('test-') || path.basename(f).includes('comprehensive') + ); + + // Features that are known to be in development + const developmentFeatures = [ + 'мавҳум', // abstract + 'номфазо', // namespace + 'калидҳои', // keyof + 'инфер', // infer + 'беназир', // unique + ]; + + // Helper to check if example uses development features + function usesDevelopmentFeatures(source: string): boolean { + return developmentFeatures.some(feature => source.includes(feature)); + } - const moduleExamples = exampleFiles.filter(f => f.includes('modules/')); + describe('Basic Examples (01-09)', () => { + test.each(basicExamples)('should compile %s', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const testExamples = exampleFiles.filter( - f => path.basename(f).startsWith('test-') || path.basename(f).includes('comprehensive') - ); + const result = compile(source, { + sourceMap: false, + }); - // Features that are known to be in development - const developmentFeatures = [ - 'мавҳум', // abstract - 'номфазо', // namespace - 'калидҳои', // keyof - 'инфер', // infer - 'беназир', // unique - ]; - - // Helper to check if example uses development features - function usesDevelopmentFeatures(source: string): boolean { - return developmentFeatures.some(feature => source.includes(feature)); - } + // Basic examples should compile without errors + expect(result.errors).toEqual([]); + expect(result.code).toBeTruthy(); + expect(result.code.length).toBeGreaterThan(0); + }); + }); - describe('Basic Examples (01-09)', () => { - test.each(basicExamples)('should compile %s', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + describe('Advanced Examples (10-25)', () => { + test.each(advancedExamples)('should handle %s', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const result = compile(source, { - sourceMap: false, - }); + const result = compile(source, { + sourceMap: false, + }); - // Basic examples should compile without errors + // Some advanced features might have warnings or partial implementation + if (usesDevelopmentFeatures(source)) { + // For development features, we expect at least some code generation + expect(result.code).toBeTruthy(); + // May have warnings but shouldn't completely fail + expect(result.warnings).toBeDefined(); + } else { + // Fully implemented features should have no errors expect(result.errors).toEqual([]); expect(result.code).toBeTruthy(); - expect(result.code.length).toBeGreaterThan(0); - }); + } }); + }); - describe('Advanced Examples (10-25)', () => { - test.each(advancedExamples)('should handle %s', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); - - const result = compile(source, { - sourceMap: false, - }); - - // Some advanced features might have warnings or partial implementation - if (usesDevelopmentFeatures(source)) { - // For development features, we expect at least some code generation - expect(result.code).toBeTruthy(); - // May have warnings but shouldn't completely fail - expect(result.warnings).toBeDefined(); - } else { - // Fully implemented features should have no errors - expect(result.errors).toEqual([]); - expect(result.code).toBeTruthy(); - } - }); + describe('New Examples (26-33)', () => { + const newExamples = exampleFiles.filter(f => { + const basename = path.basename(f); + const match = basename.match(/^(\d+)-/); + return match && Number.parseInt(match[1]) >= 26 && Number.parseInt(match[1]) <= 33; }); - describe('New Examples (26-33)', () => { - const newExamples = exampleFiles.filter(f => { - const basename = path.basename(f); - const match = basename.match(/^(\d+)-/); - return match && Number.parseInt(match[1]) >= 26 && Number.parseInt(match[1]) <= 33; - }); - - // Helper to validate feature-specific patterns - function validateExampleFeatures(fileName: string, code: string): void { - const validations = [ - { match: ['switch', '26'], pattern: /switch|if/, desc: 'Switch/case' }, - { match: ['abstract', '27'], pattern: /class|function/, desc: 'Abstract classes' }, - { match: ['namespace', '28'], pattern: /var|const|let/, desc: 'Namespaces' }, - { match: ['generic', '29'], pattern: /function|class/, desc: 'Generics' }, - { match: ['built-in', '30'], pattern: /Object|Math|console/, desc: 'Built-in objects' }, - { match: ['break-continue', '32'], pattern: /break|continue/, desc: 'Break/continue' }, - { match: ['console', '33'], pattern: /console\./, desc: 'Console methods' }, - ]; - - for (const validation of validations) { - if (validation.match.some(keyword => fileName.includes(keyword))) { - expect(code).toMatch(validation.pattern); - return; - } + // Helper to validate feature-specific patterns + function validateExampleFeatures(fileName: string, code: string): void { + const validations = [ + { match: ['switch', '26'], pattern: /switch|if/, desc: 'Switch/case' }, + { match: ['abstract', '27'], pattern: /class|function/, desc: 'Abstract classes' }, + { match: ['namespace', '28'], pattern: /var|const|let/, desc: 'Namespaces' }, + { match: ['generic', '29'], pattern: /function|class/, desc: 'Generics' }, + { match: ['built-in', '30'], pattern: /Object|Math|console/, desc: 'Built-in objects' }, + { match: ['break-continue', '32'], pattern: /break|continue/, desc: 'Break/continue' }, + { match: ['console', '33'], pattern: /console\./, desc: 'Console methods' }, + ]; + + for (const validation of validations) { + if (validation.match.some(keyword => fileName.includes(keyword))) { + expect(code).toMatch(validation.pattern); + return; } - - // Default: just check that code exists - expect(code).toBeTruthy(); } - test.each(newExamples)('should handle %s appropriately', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); - - const result = compile(source, { - sourceMap: false, - }); + // Default: just check that code exists + expect(code).toBeTruthy(); + } - // These are new examples that demonstrate patterns - // They should at least produce some output (or have no errors) - // Allow empty code if there are compilation errors - if (result.errors.length > 0) { - expect(result.code).toBeDefined(); - } else { - expect(result.code).toBeTruthy(); + test.each(newExamples)('should handle %s appropriately', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - // Check for specific features in each example - const fileName = path.basename(exampleFile, '.som'); - validateExampleFeatures(fileName, result.code); - } + const result = compile(source, { + sourceMap: false, }); + + // These are new examples that demonstrate patterns + // They should at least produce some output (or have no errors) + // Allow empty code if there are compilation errors + if (result.errors.length > 0) { + expect(result.code).toBeDefined(); + } else { + expect(result.code).toBeTruthy(); + + // Check for specific features in each example + const fileName = path.basename(exampleFile, '.som'); + validateExampleFeatures(fileName, result.code); + } }); + }); - describe('Module System Examples', () => { - test.each(moduleExamples)('should compile module %s', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + describe('Module System Examples', () => { + test.each(moduleExamples)('should compile module %s', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const result = compile(source, {}); + const result = compile(source, {}); - // Module examples might have import/export - if (source.includes('ворид') || source.includes('содир')) { - // Only check for import/export if compilation succeeded - if (result.errors.length === 0) { - expect(result.code).toBeTruthy(); - // Should translate to require/exports or import/export - if (source.includes('ворид')) { - expect(result.code).toMatch(/require|import/); - } - if (source.includes('содир')) { - expect(result.code).toMatch(/exports|export|module\.exports/); - } - } else { - // If there are errors, just check that we tried to compile - expect(result.code).toBeDefined(); + // Module examples might have import/export + if (source.includes('ворид') || source.includes('содир')) { + // Only check for import/export if compilation succeeded + if (result.errors.length === 0) { + expect(result.code).toBeTruthy(); + // Should translate to require/exports or import/export + if (source.includes('ворид')) { + expect(result.code).toMatch(/require|import/); + } + if (source.includes('содир')) { + expect(result.code).toMatch(/exports|export|module\.exports/); } } else { - // Regular module file - expect(result.code).toBeTruthy(); + // If there are errors, just check that we tried to compile + expect(result.code).toBeDefined(); } - }); + } else { + // Regular module file + expect(result.code).toBeTruthy(); + } }); + }); - describe('Test Examples', () => { - test.each(testExamples)('should compile test file %s', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + describe('Test Examples', () => { + test.each(testExamples)('should compile test file %s', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const result = compile(source, {}); + const result = compile(source, {}); - // Test files should at least produce output - expect(result.code).toBeTruthy(); - }); + // Test files should at least produce output + expect(result.code).toBeTruthy(); + }); + }); + + describe('JavaScript Output Validation', () => { + // Only test examples that should produce valid JS + const validJsExamples = exampleFiles.filter(f => { + const source = fs.readFileSync(path.join(examplesDir, f), 'utf-8'); + // Skip files with known issues or development features + return ( + !usesDevelopmentFeatures(source) && + !f.includes('comprehensive-phase3') && + !f.includes('advanced-type') + ); }); - describe('JavaScript Output Validation', () => { - // Only test examples that should produce valid JS - const validJsExamples = exampleFiles.filter(f => { - const source = fs.readFileSync(path.join(examplesDir, f), 'utf-8'); - // Skip files with known issues or development features - return ( - !usesDevelopmentFeatures(source) && - !f.includes('comprehensive-phase3') && - !f.includes('advanced-type') - ); - }); - - test.each(validJsExamples.slice(0, 20))( - // Test first 20 for performance - 'should produce valid JavaScript for %s', - exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + test.each(validJsExamples.slice(0, 20))( + // Test first 20 for performance + 'should produce valid JavaScript for %s', + exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const result = compile(source, {}); + const result = compile(source, {}); - if (result.errors.length === 0 && result.code) { - // Try to parse as JavaScript (not execute) - try { - new Function(result.code); - expect(true).toBe(true); // Valid JS - } catch (error: any) { - // Some valid patterns might still fail Function constructor - // Check for common JS patterns instead - expect(result.code).toMatch(/var|let|const|function|class|if|for|while/); - } + if (result.errors.length === 0 && result.code) { + // Try to parse as JavaScript (not execute) + try { + new Function(result.code); + expect(true).toBe(true); // Valid JS + } catch (error: any) { + // Some valid patterns might still fail Function constructor + // Check for common JS patterns instead + expect(result.code).toMatch(/var|let|const|function|class|if|for|while/); } } - ); - }); + } + ); + }); - describe('Compilation Performance', () => { - test('all examples should compile within reasonable time', () => { - const startTime = Date.now(); - let compiledCount = 0; + describe('Compilation Performance', () => { + test('all examples should compile within reasonable time', () => { + const startTime = Date.now(); + let compiledCount = 0; - for (const exampleFile of exampleFiles) { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + for (const exampleFile of exampleFiles) { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - compile(source, {}); + compile(source, {}); - compiledCount++; - } + compiledCount++; + } - const endTime = Date.now(); - const totalTime = endTime - startTime; + const endTime = Date.now(); + const totalTime = endTime - startTime; - expect(compiledCount).toBe(exampleFiles.length); - expect(totalTime).toBeLessThan(10000); // 10 seconds for all examples + expect(compiledCount).toBe(exampleFiles.length); + expect(totalTime).toBeLessThan(10000); // 10 seconds for all examples - console.log(`Compiled ${compiledCount} examples in ${totalTime}ms`); - console.log(`Average: ${(totalTime / compiledCount).toFixed(2)}ms per file`); - }); + console.log(`Compiled ${compiledCount} examples in ${totalTime}ms`); + console.log(`Average: ${(totalTime / compiledCount).toFixed(2)}ms per file`); }); + }); + + describe('Feature Coverage', () => { + test('examples cover major language features', () => { + const allContent = exampleFiles + .map(f => fs.readFileSync(path.join(examplesDir, f), 'utf-8')) + .join('\n'); + + const features = { + Variables: /тағйирёбанда/, + Constants: /собит/, + Functions: /функсия/, + Classes: /синф/, + Interfaces: /интерфейс/, + 'If statements': /агар/, + 'For loops': /барои/, + 'While loops': /то/, + Imports: /ворид/, + Exports: /содир/, + Console: /чоп\./, + Arrays: /\[.*\]/, + Objects: /\{.*\}/, + 'Try-catch': /кӯшиш/, + Async: /ҳамзамон/, + Types: /:\s*(сатр|рақам|мантиқӣ)/, + 'New operator': /нав/, + 'This keyword': /ин\./, + Return: /бозгашт/, + Break: /шикастан/, + Continue: /давом/, + Switch: /интихоб/, + }; + + const coverage: Record = {}; + + for (const [feature, pattern] of Object.entries(features)) { + coverage[feature] = pattern.test(allContent); + } - describe('Feature Coverage', () => { - test('examples cover major language features', () => { - const allContent = exampleFiles - .map(f => fs.readFileSync(path.join(examplesDir, f), 'utf-8')) - .join('\n'); - - const features = { - Variables: /тағйирёбанда/, - Constants: /собит/, - Functions: /функсия/, - Classes: /синф/, - Interfaces: /интерфейс/, - 'If statements': /агар/, - 'For loops': /барои/, - 'While loops': /то/, - Imports: /ворид/, - Exports: /содир/, - Console: /чоп\./, - Arrays: /\[.*\]/, - Objects: /\{.*\}/, - 'Try-catch': /кӯшиш/, - Async: /ҳамзамон/, - Types: /:\s*(сатр|рақам|мантиқӣ)/, - 'New operator': /нав/, - 'This keyword': /ин\./, - Return: /бозгашт/, - Break: /шикастан/, - Continue: /давом/, - Switch: /интихоб/, - }; - - const coverage: Record = {}; - - for (const [feature, pattern] of Object.entries(features)) { - coverage[feature] = pattern.test(allContent); - } - - const coveredFeatures = Object.values(coverage).filter(v => v).length; - const totalFeatures = Object.keys(coverage).length; - const coveragePercent = (coveredFeatures / totalFeatures) * 100; + const coveredFeatures = Object.values(coverage).filter(v => v).length; + const totalFeatures = Object.keys(coverage).length; + const coveragePercent = (coveredFeatures / totalFeatures) * 100; - console.log( - `Feature coverage: ${coveredFeatures}/${totalFeatures} (${coveragePercent.toFixed(1)}%)` - ); + console.log( + `Feature coverage: ${coveredFeatures}/${totalFeatures} (${coveragePercent.toFixed(1)}%)` + ); - // Log missing features - const missingFeatures = Object.entries(coverage) - .filter(([_, covered]) => !covered) - .map(([feature]) => feature); + // Log missing features + const missingFeatures = Object.entries(coverage) + .filter(([_, covered]) => !covered) + .map(([feature]) => feature); - if (missingFeatures.length > 0) { - console.log('Missing features:', missingFeatures.join(', ')); - } + if (missingFeatures.length > 0) { + console.log('Missing features:', missingFeatures.join(', ')); + } - expect(coveragePercent).toBeGreaterThan(80); // At least 80% feature coverage - }); + expect(coveragePercent).toBeGreaterThan(80); // At least 80% feature coverage }); + }); - describe('Error Handling Examples', () => { - test('error handling examples demonstrate proper patterns', () => { - const errorExamples = exampleFiles.filter(f => f.includes('error') || f.includes('14-')); + describe('Error Handling Examples', () => { + test('error handling examples demonstrate proper patterns', () => { + const errorExamples = exampleFiles.filter(f => f.includes('error') || f.includes('14-')); - expect(errorExamples.length).toBeGreaterThan(0); + expect(errorExamples.length).toBeGreaterThan(0); - for (const example of errorExamples) { - const filePath = path.join(examplesDir, example); - const source = fs.readFileSync(filePath, 'utf-8'); + for (const example of errorExamples) { + const filePath = path.join(examplesDir, example); + const source = fs.readFileSync(filePath, 'utf-8'); - // Should have error handling patterns - const hasErrorPatterns = - source.includes('партофтан') || // throw - source.includes('кӯшиш') || // try - source.includes('гирифтан') || // catch - source.includes('Хато'); // Error + // Should have error handling patterns + const hasErrorPatterns = + source.includes('партофтан') || // throw + source.includes('кӯшиш') || // try + source.includes('гирифтан') || // catch + source.includes('Хато'); // Error - expect(hasErrorPatterns).toBe(true); + expect(hasErrorPatterns).toBe(true); - const result = compile(source, {}); + const result = compile(source, {}); - expect(result.code).toBeTruthy(); - } - }); + expect(result.code).toBeTruthy(); + } }); + }); + + describe('Type System Examples', () => { + const typeExamples = exampleFiles.filter( + f => + f.includes('type') || + f.includes('interface') || + f.includes('union') || + f.includes('intersection') || + f.includes('tuple') || + f.includes('mapped') + ); - describe('Type System Examples', () => { - const typeExamples = exampleFiles.filter( - f => - f.includes('type') || - f.includes('interface') || - f.includes('union') || - f.includes('intersection') || - f.includes('tuple') || - f.includes('mapped') - ); - - test.each(typeExamples)('should compile type example %s', exampleFile => { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + test.each(typeExamples)('should compile type example %s', exampleFile => { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - const result = compile(source, { - typeCheck: true, - }); + const result = compile(source, { + typeCheck: true, + }); - // Type examples should produce output - expect(result.code).toBeTruthy(); + // Type examples should produce output + expect(result.code).toBeTruthy(); - // Check for type annotations in source - const hasTypes = /:\s*(сатр|рақам|мантиқӣ|холӣ|беқимат|ҳар)/.test(source); - if (hasTypes) { - expect(source).toMatch(/:/); // Has type annotations - } - }); + // Check for type annotations in source + const hasTypes = /:\s*(сатр|рақам|мантиқӣ|холӣ|беқимат|ҳар)/.test(source); + if (hasTypes) { + expect(source).toMatch(/:/); // Has type annotations + } }); + }); - describe('Summary Statistics', () => { - test('should provide compilation summary', () => { - const results = { - total: exampleFiles.length, - compiled: 0, - withErrors: 0, - withWarnings: 0, - failed: 0, - }; + describe('Summary Statistics', () => { + test('should provide compilation summary', () => { + const results = { + total: exampleFiles.length, + compiled: 0, + withErrors: 0, + withWarnings: 0, + failed: 0, + }; - const errorDetails: Array<{ file: string; errors: string[] }> = []; + const errorDetails: Array<{ file: string; errors: string[] }> = []; - for (const exampleFile of exampleFiles) { - const filePath = path.join(examplesDir, exampleFile); - const source = fs.readFileSync(filePath, 'utf-8'); + for (const exampleFile of exampleFiles) { + const filePath = path.join(examplesDir, exampleFile); + const source = fs.readFileSync(filePath, 'utf-8'); - try { - const result = compile(source, {}); - - if (result.code) { - results.compiled++; - } - if (result.errors.length > 0) { - results.withErrors++; - errorDetails.push({ - file: exampleFile, - errors: result.errors, - }); - } - if (result.warnings && result.warnings.length > 0) { - results.withWarnings++; - } - } catch (error) { - results.failed++; + try { + const result = compile(source, {}); + + if (result.code) { + results.compiled++; + } + if (result.errors.length > 0) { + results.withErrors++; + errorDetails.push({ + file: exampleFile, + errors: result.errors, + }); + } + if (result.warnings && result.warnings.length > 0) { + results.withWarnings++; } + } catch (error) { + results.failed++; } + } - console.log('\n=== Compilation Summary ==='); - console.log(`Total files: ${results.total}`); - console.log( - `Successfully compiled: ${results.compiled} (${((results.compiled / results.total) * 100).toFixed(1)}%)` - ); - console.log(`With errors: ${results.withErrors}`); - console.log(`With warnings: ${results.withWarnings}`); - console.log(`Failed to compile: ${results.failed}`); - - if (errorDetails.length > 0 && errorDetails.length <= 5) { - console.log('\n=== Error Details (first 5) ==='); - errorDetails.slice(0, 5).forEach(({ file, errors }) => { - console.log(`${file}:`); - errors.forEach(err => console.log(` - ${err}`)); - }); - } + console.log('\n=== Compilation Summary ==='); + console.log(`Total files: ${results.total}`); + console.log( + `Successfully compiled: ${results.compiled} (${((results.compiled / results.total) * 100).toFixed(1)}%)` + ); + console.log(`With errors: ${results.withErrors}`); + console.log(`With warnings: ${results.withWarnings}`); + console.log(`Failed to compile: ${results.failed}`); + + if (errorDetails.length > 0 && errorDetails.length <= 5) { + console.log('\n=== Error Details (first 5) ==='); + errorDetails.slice(0, 5).forEach(({ file, errors }) => { + console.log(`${file}:`); + errors.forEach(err => console.log(` - ${err}`)); + }); + } - // Most examples should compile - expect(results.compiled).toBeGreaterThan(results.total * 0.7); // At least 70% should compile - }); + // Most examples should compile + expect(results.compiled).toBeGreaterThan(results.total * 0.7); // At least 70% should compile }); - } -); + }); +}); diff --git a/tests/helpers/paths.ts b/tests/helpers/paths.ts new file mode 100644 index 0000000..6800f48 --- /dev/null +++ b/tests/helpers/paths.ts @@ -0,0 +1,70 @@ +import { execFileSync, type ExecFileSyncOptions } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +// Canonical path to the built CLI entry point shared by every CLI test. +const CLI_PATH = path.join(__dirname, '..', '..', 'dist', 'cli.js'); + +/** + * Allocate a temp directory whose path is canonicalised. + * + * On Windows, `os.tmpdir()` may return an 8.3 short-name form such as + * `C:\Users\RUNNER~1\AppData\Local\Temp`. Downstream code (path.join, + * path.resolve) typically returns the long form, which then fails + * `toContain` / `toEqual` assertions in suites that mix the two. Resolving + * through `realpathSync` forces a single canonical representation. On + * Linux/macOS the call is an identity operation. + */ +export function canonicalTmpDir(prefix: string): string { + return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix))); +} + +/** + * Invoke the compiled CLI with the given argv, without going through a shell. + * + * Using `execFileSync` sidesteps Windows cmd.exe quoting of paths with + * backslashes or spaces — an issue that previously forced these suites to be + * skipped on win32. + */ +export function runCli(args: string[], opts: ExecFileSyncOptions = {}): string { + const result = execFileSync(process.execPath, [CLI_PATH, ...args], { + encoding: 'utf-8', + ...opts, + }); + return typeof result === 'string' ? result : result.toString('utf-8'); +} + +const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); +let cachedCliPath: string | undefined; + +/** + * Resolve the CLI path, building it only when `dist/cli.js` is missing. + * + * The CI pipeline runs `npm run build` as its own step before tests, so the + * usual path is a pure existence check. When invoked locally without a + * prior build, it falls back to `npm run build` — always with an explicit + * `cwd: PROJECT_ROOT` so a leaked `process.chdir(tempDir)` from a prior + * test can't misdirect npm into a deleted temp directory (the Windows CI + * regression that blocked the original migration). + * + * On Windows `npm` is a `.cmd` shim, so we need `shell: true` there; + * elsewhere we call the binary directly. + */ +export function buildCliOnce(): string { + if (cachedCliPath) return cachedCliPath; + if (!fs.existsSync(CLI_PATH)) { + execFileSync('npm', ['run', 'build'], { + stdio: 'pipe', + shell: process.platform === 'win32', + cwd: PROJECT_ROOT, + }); + } + cachedCliPath = CLI_PATH; + return cachedCliPath; +} + +/** Absolute path to the built CLI (no build is triggered). */ +export function getCliPath(): string { + return CLI_PATH; +}