UNPKG

testify-universal-cli

Version:

Universal interactive CLI tool for scanning and executing tests across multiple programming languages

239 lines (205 loc) 8.04 kB
import { findUp } from 'find-up'; import { globby } from 'globby'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { logger } from './logger.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LANGUAGE_SUPPORT_PATH = path.join(__dirname, '..', '..', '.language-support.json'); /** * Detect the primary language of a repository * * @param {string} cwd - Current working directory * @returns {Promise<string>} - Detected language ('python', 'node', 'ruby', 'go', 'java', 'rust', or 'unknown') */ export async function detectLanguage(cwd) { logger.debug(`Detecting language in directory: ${cwd}`); // Get available languages from support file const availableLanguages = await getAvailableLanguages(); logger.trace('Available languages:', availableLanguages); // Try to detect by project files first logger.debug('Attempting to detect language by project files...'); const detectedByFiles = await detectByProjectFiles(cwd, availableLanguages); if (detectedByFiles) { logger.debug(`Language detected by project files: ${detectedByFiles}`); return detectedByFiles; } // Fall back to detecting by file extensions logger.debug('Falling back to detection by file extensions...'); const detectedByExtensions = await detectByFileExtensions(cwd, availableLanguages); if (detectedByExtensions) { logger.debug(`Language detected by file extensions: ${detectedByExtensions}`); return detectedByExtensions; } logger.warn('Could not detect language, defaulting to "unknown"'); return 'unknown'; } /** * Detect language by project files * * @param {string} cwd - Current working directory * @param {string[]} availableLanguages - Available languages * @returns {Promise<string>} - Detected language */ async function detectByProjectFiles(cwd, availableLanguages) { logger.debug(`Detecting language by project files in ${cwd}`); logger.debug(`Available languages: ${availableLanguages.join(', ')}`); // Define detection patterns for each language const detectionPatterns = { python: ['requirements.txt', 'setup.py', 'pyproject.toml', 'pytest.ini', 'conftest.py'], node: ['package.json', 'package-lock.json', 'node_modules', 'jest.config.js', 'vitest.config.js', 'vitest.config.ts', 'local_examples/**/*.test.ts', 'local_examples/**/*.test.js'], ruby: ['Gemfile', '.rspec', 'spec/spec_helper.rb', 'spec', 'Rakefile'], go: ['go.mod', 'go.sum', '*_test.go'], java: ['pom.xml', 'build.gradle', 'settings.gradle', 'src/test/java'], }; // Store language detection results const detectionResults = {}; const languageCounts = {}; // Check for each language if it's available for (const language of Object.keys(detectionPatterns)) { if (!availableLanguages.includes(language) && !availableLanguages.some(l => l.startsWith(`${language}/`))) { logger.trace(`Skipping language ${language} as it's not available`); continue; // Skip if language is not available } logger.trace(`Checking for ${language} files...`); const files = await Promise.all( detectionPatterns[language].map(async pattern => { const result = await findUp(pattern, { cwd }); if (result) { logger.trace(`Found ${language} file: ${result}`); } return result; }) ); const hasLanguage = files.some(Boolean); detectionResults[language] = hasLanguage; logger.debug(`Language ${language} detected: ${hasLanguage}`); if (hasLanguage) { // Count test files for this language languageCounts[language] = await countTestFiles(cwd, language); logger.debug(`Found ${languageCounts[language]} test files for ${language}`); } } // Get languages with detected files const detectedLanguages = Object.keys(detectionResults).filter(language => detectionResults[language]); logger.debug(`Detected languages: ${detectedLanguages.join(', ') || 'none'}`); if (detectedLanguages.length === 0) { logger.debug('No languages detected by project files'); return null; // No languages detected } if (detectedLanguages.length === 1) { logger.debug(`Single language detected: ${detectedLanguages[0]}`); return detectedLanguages[0]; } // Multiple languages detected, choose the one with more test files let primaryLanguage = detectedLanguages[0]; let maxCount = languageCounts[primaryLanguage] || 0; for (const language of detectedLanguages) { const count = languageCounts[language] || 0; if (count > maxCount) { maxCount = count; primaryLanguage = language; } } logger.debug(`Multiple languages detected, choosing ${primaryLanguage} with ${maxCount} test files`); return primaryLanguage; } /** * Detect language by file extensions * * @param {string} cwd - Current working directory * @param {string[]} availableLanguages - Available languages * @returns {Promise<string>} - Detected language */ async function detectByFileExtensions(cwd, availableLanguages) { const extensionMapping = { '.py': 'python', '.js': 'node', '.ts': 'node', '.jsx': 'node', '.tsx': 'node', '.rb': 'ruby', '.go': 'go', '.java': 'java', }; try { // Get all files in directory recursively (limit depth to avoid performance issues) const files = await globby(['**/*.*'], { cwd, deep: 3, gitignore: true, ignore: ['node_modules/**', '**/venv/**', '**/.git/**', '**/dist/**', '**/build/**'] }); // Count extensions const extensionCounts = {}; for (const file of files) { const ext = path.extname(file); if (ext && extensionMapping[ext]) { const language = extensionMapping[ext]; extensionCounts[language] = (extensionCounts[language] || 0) + 1; } } // Find most common language that is available let maxCount = 0; let primaryLanguage = 'node'; // Default to node if nothing else is found for (const [language, count] of Object.entries(extensionCounts)) { if (count > maxCount && (availableLanguages.includes(language) || availableLanguages.some(l => l.startsWith(`${language}/`)))) { maxCount = count; primaryLanguage = language; } } return primaryLanguage; } catch (error) { // Default to node if error return 'node'; } } /** * Get available languages from configuration * * @returns {Promise<string[]>} - Available languages */ async function getAvailableLanguages() { try { const data = await fs.readFile(LANGUAGE_SUPPORT_PATH, 'utf8'); const config = JSON.parse(data); return config.supportedLanguages || []; } catch (error) { // If file doesn't exist, return defaults (node and python) return ['node.js', 'python/pytest']; } } /** * Count test files based on language-specific patterns * * @param {string} cwd - Current working directory * @param {string} language - Language to count test files for * @returns {Promise<number>} - Number of test files */ async function countTestFiles(cwd, language) { try { // Define test file patterns for each language const patterns = { python: ['test_*.py', 'tests/**/*.py', '*/test_*.py', '*/tests/**/*.py'], node: ['**/*.test.js', '**/*.test.ts', '**/*.spec.js', '**/*.spec.ts', '**/test/**/*.js', '**/tests/**/*.js'], ruby: ['spec/**/*_spec.rb', 'test/**/*_test.rb'], go: ['**/*_test.go'], java: ['**/src/test/**/*.java', '**/test/**/*Test.java'] }; const languagePatterns = patterns[language] || []; let count = 0; for (const pattern of languagePatterns) { // Use platform-agnostic path handling // Instead of joining paths which could break glob patterns, // use the cwd option in globby for cross-platform compatibility const files = await globby(pattern, { cwd: cwd }); count += files.length; } return count; } catch (error) { return 0; } }