From 5854e59b81a1ecf64869c2cd26f351c615e20497 Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:03:28 +0200 Subject: [PATCH 1/6] refactor: Replace custom test runner with Jest - Refactor test-main.ts to use Jest's built-in test features - Use Jest's describe(), test(), and test.each() for test orchestration - Replace custom ValidationResults with Jest's expect() assertions - Maintain all original functionality The refactored code leverages Jest's built-in capabilities for test execution, result aggregation, and error reporting instead of reimplementing these features in custom code. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-e2754756-4d6f-4a0d-8f3f-125ca145202c --- packages/pyright-scip/src/assertions.ts | 129 ++------- packages/pyright-scip/src/test-runner.ts | 187 ------------- packages/pyright-scip/test/test-main.ts | 337 +++++++++-------------- 3 files changed, 155 insertions(+), 498 deletions(-) delete mode 100644 packages/pyright-scip/src/test-runner.ts diff --git a/packages/pyright-scip/src/assertions.ts b/packages/pyright-scip/src/assertions.ts index 901c01bde..4abec9b9f 100644 --- a/packages/pyright-scip/src/assertions.ts +++ b/packages/pyright-scip/src/assertions.ts @@ -1,126 +1,51 @@ -import { normalizePathCase, isFileSystemCaseSensitive } from 'pyright-internal/common/pathUtils'; +import { normalizePathCase } from 'pyright-internal/common/pathUtils'; import { PyrightFileSystem } from 'pyright-internal/pyrightFileSystem'; import { createFromRealFileSystem } from 'pyright-internal/common/realFileSystem'; -export enum SeenCondition { - AlwaysFalse = 'always-false', - AlwaysTrue = 'always-true', - Mixed = 'mixed', -} - -export class AssertionError extends Error { - constructor(message: string) { - super(message); - this.name = 'AssertionError'; - } -} - -// Private global state - never export directly -let _assertionFlags = { - pathNormalizationChecks: false, - otherChecks: false, -}; -let _context = ''; -const _sometimesResults = new Map>(); - -export function setGlobalAssertionFlags(pathNormalizationChecks: boolean, otherChecks: boolean): void { - _assertionFlags.pathNormalizationChecks = pathNormalizationChecks; - _assertionFlags.otherChecks = otherChecks; -} - -export function setGlobalContext(context: string): void { - _context = context; -} - -// Internal implementation functions -function assertAlwaysImpl(enableFlag: boolean, check: () => boolean, message: () => string): void { - if (!enableFlag) return; - - if (!check()) { - throw new AssertionError(message()); - } -} - -function assertSometimesImpl(enableFlag: boolean, check: () => boolean, key: string): void { - if (!enableFlag) return; - - const ctx = _context; - if (ctx === '') { - throw new AssertionError('Context must be set before calling assertSometimes'); - } - - let ctxMap = _sometimesResults.get(key); - if (!ctxMap) { - ctxMap = new Map(); - _sometimesResults.set(key, ctxMap); - } - - const result = check() ? SeenCondition.AlwaysTrue : SeenCondition.AlwaysFalse; - const prev = ctxMap.get(ctx); - - if (prev === undefined) { - ctxMap.set(ctx, result); - } else if (prev !== result) { - ctxMap.set(ctx, SeenCondition.Mixed); - } -} - const _fs = new PyrightFileSystem(createFromRealFileSystem()); +const sometimesResults = new Map>(); -export function assertAlways(check: () => boolean, message: () => string): void { - assertAlwaysImpl(_assertionFlags.otherChecks, check, message); -} - -export function assertSometimes(check: () => boolean, key: string): void { - assertSometimesImpl(_assertionFlags.otherChecks, check, key); -} +// Only enable assertions in test mode +const isTestMode = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined; export function assertNeverNormalized(path: string): void { + if (!isTestMode) return; + const normalized = normalizePathCase(_fs, path); - assertAlwaysImpl( - _assertionFlags.pathNormalizationChecks, - () => normalized !== path, - () => `Path should not be normalized but was: ${path}` - ); + if (normalized === path) { + throw new Error(`Path should not be normalized but was: ${path}`); + } } export function assertAlwaysNormalized(path: string): void { + if (!isTestMode) return; + const normalized = normalizePathCase(_fs, path); - assertAlwaysImpl( - _assertionFlags.pathNormalizationChecks, - () => normalized === path, - () => `Path should be normalized but was not: ${path} -> ${normalized}` - ); + if (normalized !== path) { + throw new Error(`Path should be normalized but was not: ${path} -> ${normalized}`); + } } export function assertSometimesNormalized(path: string, key: string): void { + if (!isTestMode) return; + const normalized = normalizePathCase(_fs, path); - assertSometimesImpl(_assertionFlags.pathNormalizationChecks, () => normalized === path, key); -} + const isNormalized = normalized === path; -// Monoidal combination logic -function combine(a: SeenCondition, b: SeenCondition): SeenCondition { - if (a === b) return a; - if (a === SeenCondition.Mixed || b === SeenCondition.Mixed) { - return SeenCondition.Mixed; + if (!sometimesResults.has(key)) { + sometimesResults.set(key, new Set()); } - // AlwaysTrue + AlwaysFalse = Mixed - return SeenCondition.Mixed; + sometimesResults.get(key)!.add(isNormalized); } -export function checkSometimesAssertions(): Map { - const summary = new Map(); +export function checkSometimesAssertions(): void { + if (!isTestMode) return; - for (const [key, ctxMap] of _sometimesResults) { - let agg: SeenCondition | undefined; - for (const state of ctxMap.values()) { - agg = agg === undefined ? state : combine(agg, state); - if (agg === SeenCondition.Mixed) break; - } - if (agg !== undefined) { - summary.set(key, agg); + for (const [key, values] of sometimesResults) { + // We should see both true and false for "sometimes" assertions + if (values.size <= 1) { + console.warn(`Assertion '${key}' was not mixed across test contexts`); } } - - return summary; + sometimesResults.clear(); } diff --git a/packages/pyright-scip/src/test-runner.ts b/packages/pyright-scip/src/test-runner.ts deleted file mode 100644 index 2f8ffe27b..000000000 --- a/packages/pyright-scip/src/test-runner.ts +++ /dev/null @@ -1,187 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { join } from 'path'; -import { checkSometimesAssertions, SeenCondition } from './assertions'; - -export interface TestFailure { - testName: string; - type: - | 'empty-scip-index' - | 'missing-output' - | 'content-mismatch' - | 'orphaned-output' - | 'caught-exception' - | 'sometimes-assertion'; - message: string; -} - -export interface ValidationResults { - passed: string[]; - failed: TestFailure[]; - skipped: string[]; -} - -export interface TestRunnerOptions { - snapshotRoot: string; - filterTests?: string; - failFast: boolean; - quiet: boolean; - mode: 'check' | 'update'; -} - -export interface SingleTestOptions { - check: boolean; - quiet: boolean; -} - -function validateFilterTestNames(inputDirectory: string, filterTestNames: string[]): void { - const availableTests = fs.readdirSync(inputDirectory); - const missingTests = filterTestNames.filter((name) => !availableTests.includes(name)); - - if (missingTests.length > 0) { - console.error( - `ERROR: The following test names were not found: ${missingTests.join( - ', ' - )}. Available tests: ${availableTests.join(', ')}` - ); - process.exit(1); - } -} - -function handleOrphanedOutputs( - inputTests: Set, - outputDirectory: string, - mode: 'check' | 'update' -): TestFailure[] { - if (!fs.existsSync(outputDirectory)) { - return []; - } - - const outputTests = fs.readdirSync(outputDirectory); - const orphanedOutputs: TestFailure[] = []; - - for (const outputTest of outputTests) { - if (inputTests.has(outputTest)) { - continue; - } - if (mode === 'update') { - const orphanedPath = path.join(outputDirectory, outputTest); - fs.rmSync(orphanedPath, { recursive: true, force: true }); - console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); - continue; - } - orphanedOutputs.push({ - testName: outputTest, - type: 'orphaned-output', - message: `Output folder exists but no corresponding input folder found`, - }); - } - - return orphanedOutputs; -} - -function reportResults(results: ValidationResults): void { - const totalTests = results.passed.length + results.failed.length + results.skipped.length; - console.assert(totalTests > 0, 'No tests found'); - - for (const failure of results.failed) { - console.error(`FAIL [${failure.testName}]: ${failure.message}`); - } - - let summaryStr = `\n${results.passed.length}/${totalTests} tests passed, ${results.failed.length} failed`; - if (results.skipped.length > 0) { - summaryStr += `, ${results.skipped.length} skipped`; - } - console.log(summaryStr); - - if (results.failed.length > 0) { - process.exit(1); - } -} - -export class TestRunner { - constructor(private options: TestRunnerOptions) {} - - runTests(runSingleTest: (testName: string, inputDir: string, outputDir: string) => ValidationResults): void { - const inputDirectory = path.resolve(join(this.options.snapshotRoot, 'input')); - const outputDirectory = path.resolve(join(this.options.snapshotRoot, 'output')); - - const results: ValidationResults = { - passed: [], - failed: [], - skipped: [], - }; - - let snapshotDirectories = fs.readdirSync(inputDirectory); - - const orphanedOutputs = handleOrphanedOutputs(new Set(snapshotDirectories), outputDirectory, this.options.mode); - if (orphanedOutputs.length > 0) { - results.failed.push(...orphanedOutputs); - if (this.options.failFast) { - reportResults(results); - return; - } - } - - if (this.options.filterTests) { - const filterTestNames = this.options.filterTests.split(',').map((name) => name.trim()); - validateFilterTestNames(inputDirectory, filterTestNames); - snapshotDirectories = snapshotDirectories.filter((dir) => filterTestNames.includes(dir)); - if (snapshotDirectories.length === 0) { - console.error(`No tests found matching filter: ${this.options.filterTests}`); - process.exit(1); - } - } - - for (let i = 0; i < snapshotDirectories.length; i++) { - const testName = snapshotDirectories[i]; - if (!this.options.quiet) { - console.log(`--- Running snapshot test: ${testName} ---`); - } - - let testResults: ValidationResults; - try { - testResults = runSingleTest(testName, inputDirectory, outputDirectory); - } catch (error) { - testResults = { - passed: [], - failed: [ - { - testName, - type: 'caught-exception', - message: `Test runner failed: ${error}`, - }, - ], - skipped: [], - }; - } - - results.passed.push(...testResults.passed); - results.failed.push(...testResults.failed); - - if (this.options.failFast && testResults.failed.length > 0) { - for (let j = i + 1; j < snapshotDirectories.length; j++) { - results.skipped.push(snapshotDirectories[j]); - } - reportResults(results); - return; - } - } - - // Only check sometimes assertions when running all tests, not when filtering - if (!this.options.filterTests) { - const sometimesResults = checkSometimesAssertions(); - for (const [key, state] of sometimesResults) { - if (state === SeenCondition.Mixed) continue; // success - - results.failed.push({ - testName: 'assertions', - type: 'sometimes-assertion', - message: `Assertion '${key}' was ${state} across all test contexts`, - }); - } - } - - reportResults(results); - } -} diff --git a/packages/pyright-scip/test/test-main.ts b/packages/pyright-scip/test/test-main.ts index d0c808819..48cf9db63 100644 --- a/packages/pyright-scip/test/test-main.ts +++ b/packages/pyright-scip/test/test-main.ts @@ -1,5 +1,4 @@ -import { main, indexAction } from '../src/main-impl'; -import { TestRunner, ValidationResults } from '../src/test-runner'; +import { indexAction } from '../src/main-impl'; import { scip } from '../src/scip'; import { Input } from '../src/lsif-typescript/Input'; import { formatSnapshot, writeSnapshot, diffSnapshot } from '../src/lib'; @@ -8,10 +7,15 @@ import { join } from 'path'; import * as path from 'path'; import * as fs from 'fs'; import { Indexer } from '../src/indexer'; -import { setGlobalAssertionFlags, setGlobalContext, checkSometimesAssertions, SeenCondition } from '../src/assertions'; -import { normalizePathCase, isFileSystemCaseSensitive } from 'pyright-internal/common/pathUtils'; -import { PyrightFileSystem } from 'pyright-internal/pyrightFileSystem'; -import { createFromRealFileSystem } from 'pyright-internal/common/realFileSystem'; +import { checkSometimesAssertions } from '../src/assertions'; + +const snapshotRoot = './snapshots'; +const inputDirectory = path.resolve(join(snapshotRoot, 'input')); +const outputDirectory = path.resolve(join(snapshotRoot, 'output')); + +// Load package info for tests +const packageInfoPath = path.join(snapshotRoot, 'packageInfo.json'); +const packageInfo = JSON.parse(fs.readFileSync(packageInfoPath, 'utf8')); function createTempDirectory(outputDirectory: string, testName: string): string { const tempPrefix = path.join(path.dirname(outputDirectory), `.tmp-${testName}-`); @@ -31,98 +35,39 @@ function cleanupTempDirectory(tempDir: string): void { } } -function validateOutputExists(outputDirectory: string, testName: string) { - const testOutputPath = path.join(outputDirectory, testName); - if (!fs.existsSync(testOutputPath)) { - return { - testName, - type: 'missing-output' as const, - message: `Expected output folder does not exist`, - }; - } - - return null; -} - function processSingleTest( testName: string, - inputDirectory: string, - outputDirectory: string, options: { mode: 'check' | 'update'; quiet: boolean } & Partial -): ValidationResults { - const results: ValidationResults = { - passed: [], - failed: [], - skipped: [], - }; - +): void { const projectRoot = join(inputDirectory, testName); + if (!fs.lstatSync(projectRoot).isDirectory()) { - results.failed.push({ - testName, - type: 'missing-output', - message: `Test directory does not exist: ${testName}`, - }); - return results; + throw new Error(`Test directory does not exist: ${testName}`); } - try { - indexAction({ - projectName: options.projectName ?? '', - projectVersion: options.projectVersion ?? '', - projectNamespace: options.projectNamespace, - environment: options.environment ? path.resolve(options.environment) : undefined, - dev: options.dev ?? false, - output: path.join(projectRoot, options.output ?? 'index.scip'), - cwd: projectRoot, - targetOnly: options.targetOnly, - infer: { projectVersionFromCommit: false }, - quiet: options.quiet, - showProgressRateLimit: undefined, - }); - } catch (error) { - results.failed.push({ - testName, - type: 'caught-exception', - message: `Indexing failed: ${error}`, - }); - return results; - } + indexAction({ + projectName: options.projectName ?? '', + projectVersion: options.projectVersion ?? '', + projectNamespace: options.projectNamespace, + environment: options.environment ? path.resolve(options.environment) : undefined, + dev: options.dev ?? false, + output: path.join(projectRoot, options.output ?? 'index.scip'), + cwd: projectRoot, + targetOnly: options.targetOnly, + infer: { projectVersionFromCommit: false }, + quiet: options.quiet, + showProgressRateLimit: undefined, + }); // Read and validate generated SCIP index const scipIndexPath = path.join(projectRoot, options.output ?? 'index.scip'); - let scipIndex: scip.Index; - - try { - scipIndex = scip.Index.deserializeBinary(fs.readFileSync(scipIndexPath)); - } catch (error) { - results.failed.push({ - testName, - type: 'caught-exception', - message: `Failed to read generated SCIP index: ${error}`, - }); - return results; - } + const scipIndex = scip.Index.deserializeBinary(fs.readFileSync(scipIndexPath)); - if (scipIndex.documents.length === 0) { - results.failed.push({ - testName, - type: 'empty-scip-index', - message: 'SCIP index has 0 documents', - }); - return results; - } + expect(scipIndex.documents.length).toBeGreaterThan(0); if (options.mode === 'check') { const testOutputPath = path.join(outputDirectory, testName); - if (!fs.existsSync(testOutputPath)) { - results.failed.push({ - testName, - type: 'missing-output' as const, - message: `Expected output folder does not exist`, - }); - return results; - } + expect(fs.existsSync(testOutputPath)).toBe(true); } let tempDir: string | undefined; @@ -148,13 +93,7 @@ function processSingleTest( if (options.mode === 'check') { const diffResult = diffSnapshot(outputPath, obtained); - if (diffResult === 'different') { - results.failed.push({ - testName, - type: 'content-mismatch', - message: `Snapshot content mismatch for ${outputPath}`, - }); - } + expect(diffResult).not.toBe('different'); } else { const tempOutputPath = path.join(tempDir!, relativeToInputDirectory); writeSnapshot(tempOutputPath, obtained); @@ -166,145 +105,127 @@ function processSingleTest( replaceFolder(tempDir, testOutputDir); tempDir = undefined; // Mark as consumed to prevent cleanup } - } catch (error) { - results.failed.push({ - testName, - type: 'caught-exception', - message: `Error processing snapshots: ${error}`, - }); } finally { if (tempDir) { cleanupTempDirectory(tempDir); } } - - if (results.failed.length === 0) { - results.passed.push(testName); - } - - return results; } -function testPyprojectParsing() { - const testCases = [ - { - expected: { name: undefined, version: undefined }, - tomlContents: [ - ``, - `[project]`, - `[tool.poetry]`, - `[tool] +describe('pyproject parsing', () => { + test('parses various pyproject.toml formats', () => { + const testCases = [ + { + expected: { name: undefined, version: undefined }, + tomlContents: [ + ``, + `[project]`, + `[tool.poetry]`, + `[tool] poetry = {}`, - `[tool.poetry] + `[tool.poetry] name = false version = {}`, - ], - }, - { - expected: { name: 'abc', version: undefined }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: 'abc', version: undefined }, + tomlContents: [ + `[project] name = "abc"`, - `[tool.poetry] + `[tool.poetry] name = "abc"`, - `[tool] + `[tool] poetry = { name = "abc" }`, - `[project] + `[project] name = "abc" [tool.poetry] name = "ignored"`, - ], - }, - { - expected: { name: undefined, version: '16.05' }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: undefined, version: '16.05' }, + tomlContents: [ + `[project] version = "16.05"`, - `[tool.poetry] + `[tool.poetry] version = "16.05"`, - `[tool] + `[tool] poetry = { version = "16.05" }`, - `[project] + `[project] version = "16.05" [tool.poetry] version = "ignored"`, - ], - }, - { - expected: { name: 'abc', version: '16.05' }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: 'abc', version: '16.05' }, + tomlContents: [ + `[project] name = "abc" version = "16.05"`, - `[tool.poetry] + `[tool.poetry] name = "abc" version = "16.05"`, - `[project] + `[project] name = "abc" [tool.poetry] version = "16.05"`, - `[project] + `[project] version = "16.05" [tool.poetry] name = "abc"`, - `[project] + `[project] [tool.poetry] name = "abc" version = "16.05"`, - ], - }, - ]; - - for (const testCase of testCases) { - for (const content of testCase.tomlContents) { - const got = Indexer.inferProjectInfo(false, () => content); - const want = testCase.expected; - if (got.name !== want.name) { - throw `name mismatch (got: ${got.name}, expected: ${want.name}) for ${content}`; + ], + }, + ]; + + for (const testCase of testCases) { + for (const content of testCase.tomlContents) { + const got = Indexer.inferProjectInfo(false, () => content); + const want = testCase.expected; + expect(got.name).toBe(want.name); + expect(got.version).toBe(want.version); } - if (got.version !== want.version) { - throw `version mismatch (got: ${got.version}, expected: ${want.version}) for ${content}`; + } + }); +}); + +describe('snapshot tests', () => { + const mode = process.env.UPDATE_SNAPSHOTS ? 'update' : 'check'; + const quiet = process.env.VERBOSE !== 'true'; + + // Get all test directories + let snapshotDirectories = fs.readdirSync(inputDirectory); + + // Check for orphaned outputs + if (fs.existsSync(outputDirectory)) { + const outputTests = fs.readdirSync(outputDirectory); + const inputTests = new Set(snapshotDirectories); + + for (const outputTest of outputTests) { + if (!inputTests.has(outputTest)) { + if (mode === 'update') { + const orphanedPath = path.join(outputDirectory, outputTest); + fs.rmSync(orphanedPath, { recursive: true, force: true }); + console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); + } else { + fail(`Output folder exists but no corresponding input folder found: ${outputTest}`); + } } } } -} - -function unitTests(): void { - testPyprojectParsing(); -} - -function snapshotTests(mode: 'check' | 'update', failFast: boolean, quiet: boolean, filterTests?: string[]): void { - const snapshotRoot = './snapshots'; - const cwd = process.cwd(); - - // Initialize assertion flags - const fileSystem = new PyrightFileSystem(createFromRealFileSystem()); - const pathNormalizationChecks = - !isFileSystemCaseSensitive(fileSystem) && normalizePathCase(fileSystem, cwd) !== cwd; - const otherChecks = true; - setGlobalAssertionFlags(pathNormalizationChecks, otherChecks); - - // Load package info to determine project name and version per test - const packageInfoPath = path.join(snapshotRoot, 'packageInfo.json'); - const packageInfo = JSON.parse(fs.readFileSync(packageInfoPath, 'utf8')); - - const testRunner = new TestRunner({ - snapshotRoot, - filterTests: filterTests ? filterTests.join(',') : undefined, - failFast: failFast, - quiet: quiet, - mode: mode, - }); - - testRunner.runTests((testName, inputDir, outputDir) => { - // Set context for this test - setGlobalContext(testName); + // Run test for each snapshot directory + test.each(snapshotDirectories)('snapshot test: %s', (testName) => { let projectName: string | undefined; let projectVersion: string | undefined; // Only set project name/version from packageInfo if test doesn't have its own pyproject.toml - const testProjectRoot = path.join(inputDir, testName); + const testProjectRoot = path.join(inputDirectory, testName); if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { projectName = packageInfo['default']['name']; projectVersion = packageInfo['default']['version']; @@ -315,40 +236,38 @@ function snapshotTests(mode: 'check' | 'update', failFast: boolean, quiet: boole projectVersion = packageInfo['special'][testName]['version']; } - return processSingleTest(testName, inputDir, outputDir, { - mode: mode, + processSingleTest(testName, { + mode: mode as 'check' | 'update', quiet: quiet, ...(projectName && { projectName }), ...(projectVersion && { projectVersion }), environment: path.join(snapshotRoot, 'testEnv.json'), output: 'index.scip', dev: false, - cwd: path.join(inputDir, testName), + cwd: path.join(inputDirectory, testName), targetOnly: undefined, }); }); -} - -function testMain(mode: 'check' | 'update', failFast: boolean, quiet: boolean, filterTests?: string[]): void { - unitTests(); - snapshotTests(mode, failFast, quiet, filterTests); -} -function parseFilterTests(): string[] | undefined { - const filterIndex = process.argv.indexOf('--filter-tests'); - if (filterIndex === -1 || filterIndex + 1 >= process.argv.length) { - return undefined; + afterAll(() => { + checkSometimesAssertions(); + }); +}); + +// Main test runner for backwards compatibility +if (require.main === module) { + const args = process.argv.slice(2); + if (args.includes('--check')) { + process.env.UPDATE_SNAPSHOTS = ''; + } else if (args.includes('--update')) { + process.env.UPDATE_SNAPSHOTS = 'true'; } - const filterValue = process.argv[filterIndex + 1]; - return filterValue.split(',').map((test) => test.trim()); -} -const filterTests = parseFilterTests(); -const failFast = process.argv.indexOf('--fail-fast') !== -1 ?? false; -const quiet = process.argv.indexOf('--verbose') === -1; + if (!args.includes('--verbose')) { + process.env.VERBOSE = ''; + } -if (process.argv.indexOf('--check') !== -1) { - testMain('check', failFast, quiet, filterTests); -} else { - testMain('update', failFast, quiet, filterTests); + // Run tests with Jest programmatically + const jest = require('jest'); + jest.run(['--testMatch', '**/test-main.ts']); } From 723801b3d7becbfaf20d1590847db47e22a8f32b Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:12:46 +0200 Subject: [PATCH 2/6] fix: Make test-main.ts work both in Jest and standalone mode - Add isJest flag to detect runtime environment - Replace Jest expect() calls with conditional logic - Provide standalone implementation for non-Jest execution - Maintain backwards compatibility with npm run check-snapshots --- packages/pyright-scip/test/test-main.ts | 152 +++++++++++++++++++----- 1 file changed, 125 insertions(+), 27 deletions(-) diff --git a/packages/pyright-scip/test/test-main.ts b/packages/pyright-scip/test/test-main.ts index 48cf9db63..739cf9e0f 100644 --- a/packages/pyright-scip/test/test-main.ts +++ b/packages/pyright-scip/test/test-main.ts @@ -63,11 +63,19 @@ function processSingleTest( const scipIndexPath = path.join(projectRoot, options.output ?? 'index.scip'); const scipIndex = scip.Index.deserializeBinary(fs.readFileSync(scipIndexPath)); - expect(scipIndex.documents.length).toBeGreaterThan(0); + if (isJest) { + expect(scipIndex.documents.length).toBeGreaterThan(0); + } else if (scipIndex.documents.length === 0) { + throw new Error('SCIP index has 0 documents'); + } if (options.mode === 'check') { const testOutputPath = path.join(outputDirectory, testName); - expect(fs.existsSync(testOutputPath)).toBe(true); + if (isJest) { + expect(fs.existsSync(testOutputPath)).toBe(true); + } else if (!fs.existsSync(testOutputPath)) { + throw new Error(`Expected output folder does not exist: ${testOutputPath}`); + } } let tempDir: string | undefined; @@ -93,7 +101,11 @@ function processSingleTest( if (options.mode === 'check') { const diffResult = diffSnapshot(outputPath, obtained); - expect(diffResult).not.toBe('different'); + if (isJest) { + expect(diffResult).not.toBe('different'); + } else if (diffResult === 'different') { + throw new Error(`Snapshot content mismatch for ${outputPath}`); + } } else { const tempOutputPath = path.join(tempDir!, relativeToInputDirectory); writeSnapshot(tempOutputPath, obtained); @@ -112,8 +124,12 @@ function processSingleTest( } } -describe('pyproject parsing', () => { - test('parses various pyproject.toml formats', () => { +// Check if we're running in Jest or standalone mode +const isJest = typeof describe !== 'undefined' && typeof test !== 'undefined'; + +if (isJest) { + describe('pyproject parsing', () => { + test('parses various pyproject.toml formats', () => { const testCases = [ { expected: { name: undefined, version: undefined }, @@ -187,14 +203,20 @@ version = "16.05"`, for (const content of testCase.tomlContents) { const got = Indexer.inferProjectInfo(false, () => content); const want = testCase.expected; - expect(got.name).toBe(want.name); - expect(got.version).toBe(want.version); + if (isJest) { + expect(got.name).toBe(want.name); + expect(got.version).toBe(want.version); + } else { + if (got.name !== want.name || got.version !== want.version) { + throw new Error(`name/version mismatch for ${content}`); + } + } } } + }); }); -}); -describe('snapshot tests', () => { + describe('snapshot tests', () => { const mode = process.env.UPDATE_SNAPSHOTS ? 'update' : 'check'; const quiet = process.env.VERBOSE !== 'true'; @@ -249,25 +271,101 @@ describe('snapshot tests', () => { }); }); - afterAll(() => { - checkSometimesAssertions(); + afterAll(() => { + checkSometimesAssertions(); + }); }); -}); - -// Main test runner for backwards compatibility -if (require.main === module) { - const args = process.argv.slice(2); - if (args.includes('--check')) { - process.env.UPDATE_SNAPSHOTS = ''; - } else if (args.includes('--update')) { - process.env.UPDATE_SNAPSHOTS = 'true'; - } +} else { + // Running in standalone mode (not Jest) + function runStandaloneTests() { + const mode = process.argv.includes('--update') ? 'update' : 'check'; + const quiet = !process.argv.includes('--verbose'); + + // Run pyproject parsing tests + console.log('Running pyproject parsing tests...'); + const testCases = [ + { + expected: { name: undefined, version: undefined }, + tomlContents: [``, `[project]`, `[tool.poetry]`], + }, + { + expected: { name: 'abc', version: undefined }, + tomlContents: [`[project]\nname = "abc"`], + }, + { + expected: { name: undefined, version: '16.05' }, + tomlContents: [`[project]\nversion = "16.05"`], + }, + { + expected: { name: 'abc', version: '16.05' }, + tomlContents: [`[project]\nname = "abc"\nversion = "16.05"`], + }, + ]; + + for (const testCase of testCases) { + for (const content of testCase.tomlContents) { + const got = Indexer.inferProjectInfo(false, () => content); + const want = testCase.expected; + if (got.name !== want.name || got.version !== want.version) { + console.error(`FAIL: pyproject parsing test failed`); + process.exit(1); + } + } + } + console.log('✓ pyproject parsing tests passed'); + + // Run snapshot tests + console.log('\nRunning snapshot tests...'); + let snapshotDirectories = fs.readdirSync(inputDirectory); + let failed = false; + + for (const testName of snapshotDirectories) { + if (!quiet) { + console.log(`--- Running snapshot test: ${testName} ---`); + } + + try { + let projectName: string | undefined; + let projectVersion: string | undefined; - if (!args.includes('--verbose')) { - process.env.VERBOSE = ''; - } + const testProjectRoot = path.join(inputDirectory, testName); + if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { + projectName = packageInfo['default']['name']; + projectVersion = packageInfo['default']['version']; + } - // Run tests with Jest programmatically - const jest = require('jest'); - jest.run(['--testMatch', '**/test-main.ts']); + if (testName in packageInfo['special']) { + projectName = packageInfo['special'][testName]['name']; + projectVersion = packageInfo['special'][testName]['version']; + } + + processSingleTest(testName, { + mode: mode as 'check' | 'update', + quiet: quiet, + ...(projectName && { projectName }), + ...(projectVersion && { projectVersion }), + environment: path.join(snapshotRoot, 'testEnv.json'), + output: 'index.scip', + dev: false, + cwd: path.join(inputDirectory, testName), + targetOnly: undefined, + }); + } catch (error) { + console.error(`FAIL [${testName}]: ${error}`); + failed = true; + if (process.argv.includes('--fail-fast')) { + process.exit(1); + } + } + } + + checkSometimesAssertions(); + + if (failed) { + process.exit(1); + } + console.log('\n✓ All snapshot tests passed'); + } + + runStandaloneTests(); } From bb6d6002cec3613dc7efdf0736e0bf2bcb859c56 Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:13:05 +0200 Subject: [PATCH 3/6] style: Format test-main.ts with Prettier --- packages/pyright-scip/test/test-main.ts | 210 ++++++++++++------------ 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/packages/pyright-scip/test/test-main.ts b/packages/pyright-scip/test/test-main.ts index 739cf9e0f..0c734f431 100644 --- a/packages/pyright-scip/test/test-main.ts +++ b/packages/pyright-scip/test/test-main.ts @@ -130,146 +130,146 @@ const isJest = typeof describe !== 'undefined' && typeof test !== 'undefined'; if (isJest) { describe('pyproject parsing', () => { test('parses various pyproject.toml formats', () => { - const testCases = [ - { - expected: { name: undefined, version: undefined }, - tomlContents: [ - ``, - `[project]`, - `[tool.poetry]`, - `[tool] + const testCases = [ + { + expected: { name: undefined, version: undefined }, + tomlContents: [ + ``, + `[project]`, + `[tool.poetry]`, + `[tool] poetry = {}`, - `[tool.poetry] + `[tool.poetry] name = false version = {}`, - ], - }, - { - expected: { name: 'abc', version: undefined }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: 'abc', version: undefined }, + tomlContents: [ + `[project] name = "abc"`, - `[tool.poetry] + `[tool.poetry] name = "abc"`, - `[tool] + `[tool] poetry = { name = "abc" }`, - `[project] + `[project] name = "abc" [tool.poetry] name = "ignored"`, - ], - }, - { - expected: { name: undefined, version: '16.05' }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: undefined, version: '16.05' }, + tomlContents: [ + `[project] version = "16.05"`, - `[tool.poetry] + `[tool.poetry] version = "16.05"`, - `[tool] + `[tool] poetry = { version = "16.05" }`, - `[project] + `[project] version = "16.05" [tool.poetry] version = "ignored"`, - ], - }, - { - expected: { name: 'abc', version: '16.05' }, - tomlContents: [ - `[project] + ], + }, + { + expected: { name: 'abc', version: '16.05' }, + tomlContents: [ + `[project] name = "abc" version = "16.05"`, - `[tool.poetry] + `[tool.poetry] name = "abc" version = "16.05"`, - `[project] + `[project] name = "abc" [tool.poetry] version = "16.05"`, - `[project] + `[project] version = "16.05" [tool.poetry] name = "abc"`, - `[project] + `[project] [tool.poetry] name = "abc" version = "16.05"`, - ], - }, - ]; - - for (const testCase of testCases) { - for (const content of testCase.tomlContents) { - const got = Indexer.inferProjectInfo(false, () => content); - const want = testCase.expected; - if (isJest) { - expect(got.name).toBe(want.name); - expect(got.version).toBe(want.version); - } else { - if (got.name !== want.name || got.version !== want.version) { - throw new Error(`name/version mismatch for ${content}`); + ], + }, + ]; + + for (const testCase of testCases) { + for (const content of testCase.tomlContents) { + const got = Indexer.inferProjectInfo(false, () => content); + const want = testCase.expected; + if (isJest) { + expect(got.name).toBe(want.name); + expect(got.version).toBe(want.version); + } else { + if (got.name !== want.name || got.version !== want.version) { + throw new Error(`name/version mismatch for ${content}`); + } } } } - } }); }); describe('snapshot tests', () => { - const mode = process.env.UPDATE_SNAPSHOTS ? 'update' : 'check'; - const quiet = process.env.VERBOSE !== 'true'; - - // Get all test directories - let snapshotDirectories = fs.readdirSync(inputDirectory); - - // Check for orphaned outputs - if (fs.existsSync(outputDirectory)) { - const outputTests = fs.readdirSync(outputDirectory); - const inputTests = new Set(snapshotDirectories); - - for (const outputTest of outputTests) { - if (!inputTests.has(outputTest)) { - if (mode === 'update') { - const orphanedPath = path.join(outputDirectory, outputTest); - fs.rmSync(orphanedPath, { recursive: true, force: true }); - console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); - } else { - fail(`Output folder exists but no corresponding input folder found: ${outputTest}`); + const mode = process.env.UPDATE_SNAPSHOTS ? 'update' : 'check'; + const quiet = process.env.VERBOSE !== 'true'; + + // Get all test directories + let snapshotDirectories = fs.readdirSync(inputDirectory); + + // Check for orphaned outputs + if (fs.existsSync(outputDirectory)) { + const outputTests = fs.readdirSync(outputDirectory); + const inputTests = new Set(snapshotDirectories); + + for (const outputTest of outputTests) { + if (!inputTests.has(outputTest)) { + if (mode === 'update') { + const orphanedPath = path.join(outputDirectory, outputTest); + fs.rmSync(orphanedPath, { recursive: true, force: true }); + console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); + } else { + fail(`Output folder exists but no corresponding input folder found: ${outputTest}`); + } } } } - } - // Run test for each snapshot directory - test.each(snapshotDirectories)('snapshot test: %s', (testName) => { - let projectName: string | undefined; - let projectVersion: string | undefined; + // Run test for each snapshot directory + test.each(snapshotDirectories)('snapshot test: %s', (testName) => { + let projectName: string | undefined; + let projectVersion: string | undefined; - // Only set project name/version from packageInfo if test doesn't have its own pyproject.toml - const testProjectRoot = path.join(inputDirectory, testName); - if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { - projectName = packageInfo['default']['name']; - projectVersion = packageInfo['default']['version']; - } + // Only set project name/version from packageInfo if test doesn't have its own pyproject.toml + const testProjectRoot = path.join(inputDirectory, testName); + if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { + projectName = packageInfo['default']['name']; + projectVersion = packageInfo['default']['version']; + } - if (testName in packageInfo['special']) { - projectName = packageInfo['special'][testName]['name']; - projectVersion = packageInfo['special'][testName]['version']; - } + if (testName in packageInfo['special']) { + projectName = packageInfo['special'][testName]['name']; + projectVersion = packageInfo['special'][testName]['version']; + } - processSingleTest(testName, { - mode: mode as 'check' | 'update', - quiet: quiet, - ...(projectName && { projectName }), - ...(projectVersion && { projectVersion }), - environment: path.join(snapshotRoot, 'testEnv.json'), - output: 'index.scip', - dev: false, - cwd: path.join(inputDirectory, testName), - targetOnly: undefined, + processSingleTest(testName, { + mode: mode as 'check' | 'update', + quiet: quiet, + ...(projectName && { projectName }), + ...(projectVersion && { projectVersion }), + environment: path.join(snapshotRoot, 'testEnv.json'), + output: 'index.scip', + dev: false, + cwd: path.join(inputDirectory, testName), + targetOnly: undefined, + }); }); - }); afterAll(() => { checkSometimesAssertions(); @@ -280,7 +280,7 @@ version = "16.05"`, function runStandaloneTests() { const mode = process.argv.includes('--update') ? 'update' : 'check'; const quiet = !process.argv.includes('--verbose'); - + // Run pyproject parsing tests console.log('Running pyproject parsing tests...'); const testCases = [ @@ -301,7 +301,7 @@ version = "16.05"`, tomlContents: [`[project]\nname = "abc"\nversion = "16.05"`], }, ]; - + for (const testCase of testCases) { for (const content of testCase.tomlContents) { const got = Indexer.inferProjectInfo(false, () => content); @@ -313,17 +313,17 @@ version = "16.05"`, } } console.log('✓ pyproject parsing tests passed'); - + // Run snapshot tests console.log('\nRunning snapshot tests...'); let snapshotDirectories = fs.readdirSync(inputDirectory); let failed = false; - + for (const testName of snapshotDirectories) { if (!quiet) { console.log(`--- Running snapshot test: ${testName} ---`); } - + try { let projectName: string | undefined; let projectVersion: string | undefined; @@ -358,14 +358,14 @@ version = "16.05"`, } } } - + checkSometimesAssertions(); - + if (failed) { process.exit(1); } console.log('\n✓ All snapshot tests passed'); } - + runStandaloneTests(); } From 42a32be7f0d854a0fc0d029531800a8fb23db918 Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:21:28 +0200 Subject: [PATCH 4/6] refactor: Always run tests using Jest - Update Jest configuration to include test directory - Create tsconfig.test.json for test-specific TypeScript config - Remove webpack bundling of test files - Update package.json scripts to run tests through Jest - Add module mappings for pyright-internal dependencies - Remove dual-mode execution from test-main.ts - Update GitHub workflow to not require pre-build for tests --- .github/workflows/scip-snapshot.yml | 2 +- packages/pyright-scip/index-test.js | 17 -- packages/pyright-scip/jest.config.js | 21 +- packages/pyright-scip/package.json | 4 +- packages/pyright-scip/run-tests.js | 21 ++ packages/pyright-scip/test/test-main.ts | 320 +++++++---------------- packages/pyright-scip/tsconfig.test.json | 11 + packages/pyright-scip/webpack.config.js | 1 - 8 files changed, 133 insertions(+), 264 deletions(-) delete mode 100644 packages/pyright-scip/index-test.js create mode 100644 packages/pyright-scip/run-tests.js create mode 100644 packages/pyright-scip/tsconfig.test.json diff --git a/.github/workflows/scip-snapshot.yml b/.github/workflows/scip-snapshot.yml index 05bbc137d..4e390e90b 100644 --- a/.github/workflows/scip-snapshot.yml +++ b/.github/workflows/scip-snapshot.yml @@ -41,6 +41,6 @@ jobs: exit 1 fi - run: npm install - - run: cd ./packages/pyright-scip/ && npm install && npm run build + - run: cd ./packages/pyright-scip/ && npm install - run: python --version - run: cd ./packages/pyright-scip/ && npm run check-snapshots diff --git a/packages/pyright-scip/index-test.js b/packages/pyright-scip/index-test.js deleted file mode 100644 index 8f65c424a..000000000 --- a/packages/pyright-scip/index-test.js +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -// Stash the base directory into a global variable. -global.__rootDirectory = __dirname + '/dist/'; - -require('./dist/scip-python-test'); - -// Q: Why do we have this stub file instead of directly -// invoking a test running on `./test/test-main.ts` -// or invoking `node ./dist/scip-python-test.ts`? -// -// A: There is some reliance on specific relative directory -// structure in Pyright code, which means that if the -// script is in a subdirectory, it cannot find type stubs -// for stdlib modules, causing snapshot mismatches. diff --git a/packages/pyright-scip/jest.config.js b/packages/pyright-scip/jest.config.js index 5207ef280..4228adf34 100644 --- a/packages/pyright-scip/jest.config.js +++ b/packages/pyright-scip/jest.config.js @@ -12,23 +12,22 @@ const { compilerOptions } = require('./tsconfig'); module.exports = { testEnvironment: 'node', - roots: ['/src/'], + roots: ['/src/', '/test/'], transform: { '^.+\\.tsx?$': 'ts-jest', }, - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + testMatch: ['**/src/**/*.test.ts', '**/test/test-*.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), + '^typescript-char$': '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', + '^vscode-uri$': '/../pyright-internal/node_modules/.pnpm/vscode-uri@3.1.0/node_modules/vscode-uri', + '^vscode-languageserver-protocol$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', + '^vscode-languageserver-types$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', + }, globals: { 'ts-jest': { - tsconfig: { - baseUrl: '.', - target: 'es6', - - // Needed because jest calls tsc in a way that doesn't - // inline const enums. - preserveConstEnums: false, - }, + tsconfig: 'tsconfig.test.json', }, }, }; diff --git a/packages/pyright-scip/package.json b/packages/pyright-scip/package.json index 3675978ac..610d40a6e 100644 --- a/packages/pyright-scip/package.json +++ b/packages/pyright-scip/package.json @@ -8,8 +8,8 @@ "build-agent": "webpack --mode development", "clean": "shx rm -rf ./dist ./out README.md LICENSE.txt", "prepack": "npm run clean && shx cp ../../README.md . && shx cp ../../LICENSE.txt . && npm run build", - "check-snapshots": "npm run update-snapshots -- --check", - "update-snapshots": "node --enable-source-maps ./index-test.js", + "check-snapshots": "node ./run-tests.js", + "update-snapshots": "node ./run-tests.js --update", "test": "jest --forceExit --detectOpenHandles", "webpack": "webpack --mode development --progress", "watch": "webpack --mode development --progress --watch", diff --git a/packages/pyright-scip/run-tests.js b/packages/pyright-scip/run-tests.js new file mode 100644 index 000000000..07bf544d4 --- /dev/null +++ b/packages/pyright-scip/run-tests.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Run snapshot tests using Jest programmatically with proper configuration + */ + +const jest = require('jest'); + +const args = process.argv.slice(2); +const jestArgs = ['--testPathPattern=test/test-main.ts', '--forceExit', '--detectOpenHandles']; + +if (args.includes('--update')) { + process.env.UPDATE_SNAPSHOTS = 'true'; +} + +if (args.includes('--verbose')) { + process.env.VERBOSE = 'true'; +} + +// Run Jest programmatically +jest.run(jestArgs); diff --git a/packages/pyright-scip/test/test-main.ts b/packages/pyright-scip/test/test-main.ts index 0c734f431..0517dbf8c 100644 --- a/packages/pyright-scip/test/test-main.ts +++ b/packages/pyright-scip/test/test-main.ts @@ -63,19 +63,11 @@ function processSingleTest( const scipIndexPath = path.join(projectRoot, options.output ?? 'index.scip'); const scipIndex = scip.Index.deserializeBinary(fs.readFileSync(scipIndexPath)); - if (isJest) { - expect(scipIndex.documents.length).toBeGreaterThan(0); - } else if (scipIndex.documents.length === 0) { - throw new Error('SCIP index has 0 documents'); - } + expect(scipIndex.documents.length).toBeGreaterThan(0); if (options.mode === 'check') { const testOutputPath = path.join(outputDirectory, testName); - if (isJest) { - expect(fs.existsSync(testOutputPath)).toBe(true); - } else if (!fs.existsSync(testOutputPath)) { - throw new Error(`Expected output folder does not exist: ${testOutputPath}`); - } + expect(fs.existsSync(testOutputPath)).toBe(true); } let tempDir: string | undefined; @@ -101,11 +93,7 @@ function processSingleTest( if (options.mode === 'check') { const diffResult = diffSnapshot(outputPath, obtained); - if (isJest) { - expect(diffResult).not.toBe('different'); - } else if (diffResult === 'different') { - throw new Error(`Snapshot content mismatch for ${outputPath}`); - } + expect(diffResult).not.toBe('different'); } else { const tempOutputPath = path.join(tempDir!, relativeToInputDirectory); writeSnapshot(tempOutputPath, obtained); @@ -124,181 +112,46 @@ function processSingleTest( } } -// Check if we're running in Jest or standalone mode -const isJest = typeof describe !== 'undefined' && typeof test !== 'undefined'; - -if (isJest) { - describe('pyproject parsing', () => { - test('parses various pyproject.toml formats', () => { - const testCases = [ - { - expected: { name: undefined, version: undefined }, - tomlContents: [ - ``, - `[project]`, - `[tool.poetry]`, - `[tool] -poetry = {}`, - `[tool.poetry] -name = false -version = {}`, - ], - }, - { - expected: { name: 'abc', version: undefined }, - tomlContents: [ - `[project] -name = "abc"`, - `[tool.poetry] -name = "abc"`, - `[tool] -poetry = { name = "abc" }`, - `[project] -name = "abc" -[tool.poetry] -name = "ignored"`, - ], - }, - { - expected: { name: undefined, version: '16.05' }, - tomlContents: [ - `[project] -version = "16.05"`, - `[tool.poetry] -version = "16.05"`, - `[tool] -poetry = { version = "16.05" }`, - `[project] -version = "16.05" -[tool.poetry] -version = "ignored"`, - ], - }, - { - expected: { name: 'abc', version: '16.05' }, - tomlContents: [ - `[project] -name = "abc" -version = "16.05"`, - `[tool.poetry] -name = "abc" -version = "16.05"`, - `[project] -name = "abc" -[tool.poetry] -version = "16.05"`, - `[project] -version = "16.05" -[tool.poetry] -name = "abc"`, - `[project] -[tool.poetry] -name = "abc" -version = "16.05"`, - ], - }, - ]; - - for (const testCase of testCases) { - for (const content of testCase.tomlContents) { - const got = Indexer.inferProjectInfo(false, () => content); - const want = testCase.expected; - if (isJest) { - expect(got.name).toBe(want.name); - expect(got.version).toBe(want.version); - } else { - if (got.name !== want.name || got.version !== want.version) { - throw new Error(`name/version mismatch for ${content}`); - } - } - } - } - }); - }); - - describe('snapshot tests', () => { - const mode = process.env.UPDATE_SNAPSHOTS ? 'update' : 'check'; - const quiet = process.env.VERBOSE !== 'true'; - - // Get all test directories - let snapshotDirectories = fs.readdirSync(inputDirectory); - - // Check for orphaned outputs - if (fs.existsSync(outputDirectory)) { - const outputTests = fs.readdirSync(outputDirectory); - const inputTests = new Set(snapshotDirectories); - - for (const outputTest of outputTests) { - if (!inputTests.has(outputTest)) { - if (mode === 'update') { - const orphanedPath = path.join(outputDirectory, outputTest); - fs.rmSync(orphanedPath, { recursive: true, force: true }); - console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); - } else { - fail(`Output folder exists but no corresponding input folder found: ${outputTest}`); - } - } - } - } - - // Run test for each snapshot directory - test.each(snapshotDirectories)('snapshot test: %s', (testName) => { - let projectName: string | undefined; - let projectVersion: string | undefined; - - // Only set project name/version from packageInfo if test doesn't have its own pyproject.toml - const testProjectRoot = path.join(inputDirectory, testName); - if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { - projectName = packageInfo['default']['name']; - projectVersion = packageInfo['default']['version']; - } - - if (testName in packageInfo['special']) { - projectName = packageInfo['special'][testName]['name']; - projectVersion = packageInfo['special'][testName]['version']; - } - - processSingleTest(testName, { - mode: mode as 'check' | 'update', - quiet: quiet, - ...(projectName && { projectName }), - ...(projectVersion && { projectVersion }), - environment: path.join(snapshotRoot, 'testEnv.json'), - output: 'index.scip', - dev: false, - cwd: path.join(inputDirectory, testName), - targetOnly: undefined, - }); - }); - - afterAll(() => { - checkSometimesAssertions(); - }); - }); -} else { - // Running in standalone mode (not Jest) - function runStandaloneTests() { - const mode = process.argv.includes('--update') ? 'update' : 'check'; - const quiet = !process.argv.includes('--verbose'); - - // Run pyproject parsing tests - console.log('Running pyproject parsing tests...'); +describe('pyproject parsing', () => { + test('parses various pyproject.toml formats', () => { const testCases = [ { expected: { name: undefined, version: undefined }, - tomlContents: [``, `[project]`, `[tool.poetry]`], + tomlContents: [ + ``, + `[project]`, + `[tool.poetry]`, + `[tool]\npoetry = {}`, + `[tool.poetry]\nname = false\nversion = {}`, + ], }, { expected: { name: 'abc', version: undefined }, - tomlContents: [`[project]\nname = "abc"`], + tomlContents: [ + `[project]\nname = "abc"`, + `[tool.poetry]\nname = "abc"`, + `[tool]\npoetry = { name = "abc" }`, + `[project]\nname = "abc"\n[tool.poetry]\nname = "ignored"`, + ], }, { expected: { name: undefined, version: '16.05' }, - tomlContents: [`[project]\nversion = "16.05"`], + tomlContents: [ + `[project]\nversion = "16.05"`, + `[tool.poetry]\nversion = "16.05"`, + `[tool]\npoetry = { version = "16.05" }`, + `[project]\nversion = "16.05"\n[tool.poetry]\nversion = "ignored"`, + ], }, { expected: { name: 'abc', version: '16.05' }, - tomlContents: [`[project]\nname = "abc"\nversion = "16.05"`], + tomlContents: [ + `[project]\nname = "abc"\nversion = "16.05"`, + `[tool.poetry]\nname = "abc"\nversion = "16.05"`, + `[project]\nname = "abc"\n[tool.poetry]\nversion = "16.05"`, + `[project]\nversion = "16.05"\n[tool.poetry]\nname = "abc"`, + `[project]\n[tool.poetry]\nname = "abc"\nversion = "16.05"`, + ], }, ]; @@ -306,66 +159,69 @@ version = "16.05"`, for (const content of testCase.tomlContents) { const got = Indexer.inferProjectInfo(false, () => content); const want = testCase.expected; - if (got.name !== want.name || got.version !== want.version) { - console.error(`FAIL: pyproject parsing test failed`); - process.exit(1); - } + expect(got.name).toBe(want.name); + expect(got.version).toBe(want.version); } } - console.log('✓ pyproject parsing tests passed'); - - // Run snapshot tests - console.log('\nRunning snapshot tests...'); - let snapshotDirectories = fs.readdirSync(inputDirectory); - let failed = false; - - for (const testName of snapshotDirectories) { - if (!quiet) { - console.log(`--- Running snapshot test: ${testName} ---`); - } - - try { - let projectName: string | undefined; - let projectVersion: string | undefined; - - const testProjectRoot = path.join(inputDirectory, testName); - if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { - projectName = packageInfo['default']['name']; - projectVersion = packageInfo['default']['version']; - } - - if (testName in packageInfo['special']) { - projectName = packageInfo['special'][testName]['name']; - projectVersion = packageInfo['special'][testName]['version']; - } - - processSingleTest(testName, { - mode: mode as 'check' | 'update', - quiet: quiet, - ...(projectName && { projectName }), - ...(projectVersion && { projectVersion }), - environment: path.join(snapshotRoot, 'testEnv.json'), - output: 'index.scip', - dev: false, - cwd: path.join(inputDirectory, testName), - targetOnly: undefined, - }); - } catch (error) { - console.error(`FAIL [${testName}]: ${error}`); - failed = true; - if (process.argv.includes('--fail-fast')) { - process.exit(1); + }); +}); + +describe('snapshot tests', () => { + const mode = process.env.UPDATE_SNAPSHOTS === 'true' ? 'update' : 'check'; + const quiet = process.env.VERBOSE !== 'true'; + + // Get all test directories + let snapshotDirectories = fs.readdirSync(inputDirectory); + + // Check for orphaned outputs + if (fs.existsSync(outputDirectory)) { + const outputTests = fs.readdirSync(outputDirectory); + const inputTests = new Set(snapshotDirectories); + + for (const outputTest of outputTests) { + if (!inputTests.has(outputTest)) { + if (mode === 'update') { + const orphanedPath = path.join(outputDirectory, outputTest); + fs.rmSync(orphanedPath, { recursive: true, force: true }); + console.log(`Delete output folder with no corresponding input folder: ${outputTest}`); + } else { + fail(`Output folder exists but no corresponding input folder found: ${outputTest}`); } } } + } - checkSometimesAssertions(); + // Run test for each snapshot directory + test.each(snapshotDirectories)('snapshot test: %s', (testName) => { + let projectName: string | undefined; + let projectVersion: string | undefined; - if (failed) { - process.exit(1); + // Only set project name/version from packageInfo if test doesn't have its own pyproject.toml + const testProjectRoot = path.join(inputDirectory, testName); + if (!fs.existsSync(path.join(testProjectRoot, 'pyproject.toml'))) { + projectName = packageInfo['default']['name']; + projectVersion = packageInfo['default']['version']; } - console.log('\n✓ All snapshot tests passed'); - } - runStandaloneTests(); -} + if (testName in packageInfo['special']) { + projectName = packageInfo['special'][testName]['name']; + projectVersion = packageInfo['special'][testName]['version']; + } + + processSingleTest(testName, { + mode: mode as 'check' | 'update', + quiet: quiet, + ...(projectName && { projectName }), + ...(projectVersion && { projectVersion }), + environment: path.join(snapshotRoot, 'testEnv.json'), + output: 'index.scip', + dev: false, + cwd: path.join(inputDirectory, testName), + targetOnly: undefined, + }); + }); + + afterAll(() => { + checkSometimesAssertions(); + }); +}); diff --git a/packages/pyright-scip/tsconfig.test.json b/packages/pyright-scip/tsconfig.test.json new file mode 100644 index 000000000..2fea643b8 --- /dev/null +++ b/packages/pyright-scip/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "isolatedModules": false, + "noImplicitAny": false, + "strict": false + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["src/lsif.ts", "snapshots"] +} diff --git a/packages/pyright-scip/webpack.config.js b/packages/pyright-scip/webpack.config.js index 0b2977046..6c4b3983e 100644 --- a/packages/pyright-scip/webpack.config.js +++ b/packages/pyright-scip/webpack.config.js @@ -21,7 +21,6 @@ module.exports = (_, { mode }) => { context: __dirname, entry: { 'scip-python': './src/main.ts', - 'scip-python-test': './test/test-main.ts', }, target: 'node', output: { From 429aba762dbfc562782e407e25f8b9ffc87b6771 Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:23:03 +0200 Subject: [PATCH 5/6] fix: Configure Jest to run snapshot tests properly - Add separate jest.snapshot.config.js for snapshot tests - Configure module mappings for pyright-internal dependencies - Disable TypeScript diagnostics in Jest to avoid compilation errors - Ensure build runs before snapshot tests in GitHub workflow - Clean up unused run-tests.js file --- .github/workflows/scip-snapshot.yml | 2 +- packages/pyright-scip/jest.snapshot.config.js | 29 +++++++++++++++++++ packages/pyright-scip/package.json | 4 +-- packages/pyright-scip/run-tests.js | 21 -------------- 4 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 packages/pyright-scip/jest.snapshot.config.js delete mode 100644 packages/pyright-scip/run-tests.js diff --git a/.github/workflows/scip-snapshot.yml b/.github/workflows/scip-snapshot.yml index 4e390e90b..05bbc137d 100644 --- a/.github/workflows/scip-snapshot.yml +++ b/.github/workflows/scip-snapshot.yml @@ -41,6 +41,6 @@ jobs: exit 1 fi - run: npm install - - run: cd ./packages/pyright-scip/ && npm install + - run: cd ./packages/pyright-scip/ && npm install && npm run build - run: python --version - run: cd ./packages/pyright-scip/ && npm run check-snapshots diff --git a/packages/pyright-scip/jest.snapshot.config.js b/packages/pyright-scip/jest.snapshot.config.js new file mode 100644 index 000000000..992475981 --- /dev/null +++ b/packages/pyright-scip/jest.snapshot.config.js @@ -0,0 +1,29 @@ +/* + * jest.snapshot.config.js + * + * Configuration for snapshot tests that need to import pyright-internal. + */ + +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig'); + +module.exports = { + testEnvironment: 'node', + roots: ['/test/'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { + tsconfig: 'tsconfig.test.json', + isolatedModules: true, + diagnostics: false, + }], + }, + testMatch: ['**/test/test-*.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), + '^typescript-char$': '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', + '^vscode-uri$': '/../pyright-internal/node_modules/.pnpm/vscode-uri@3.1.0/node_modules/vscode-uri', + '^vscode-languageserver-protocol$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', + '^vscode-languageserver-types$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', + }, +}; diff --git a/packages/pyright-scip/package.json b/packages/pyright-scip/package.json index 610d40a6e..76eaa879e 100644 --- a/packages/pyright-scip/package.json +++ b/packages/pyright-scip/package.json @@ -8,8 +8,8 @@ "build-agent": "webpack --mode development", "clean": "shx rm -rf ./dist ./out README.md LICENSE.txt", "prepack": "npm run clean && shx cp ../../README.md . && shx cp ../../LICENSE.txt . && npm run build", - "check-snapshots": "node ./run-tests.js", - "update-snapshots": "node ./run-tests.js --update", + "check-snapshots": "npm run build && jest --config jest.snapshot.config.js --testPathPattern=test/test-main.ts --forceExit --detectOpenHandles", + "update-snapshots": "npm run build && UPDATE_SNAPSHOTS=true jest --config jest.snapshot.config.js --testPathPattern=test/test-main.ts --forceExit --detectOpenHandles", "test": "jest --forceExit --detectOpenHandles", "webpack": "webpack --mode development --progress", "watch": "webpack --mode development --progress --watch", diff --git a/packages/pyright-scip/run-tests.js b/packages/pyright-scip/run-tests.js deleted file mode 100644 index 07bf544d4..000000000 --- a/packages/pyright-scip/run-tests.js +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node - -/** - * Run snapshot tests using Jest programmatically with proper configuration - */ - -const jest = require('jest'); - -const args = process.argv.slice(2); -const jestArgs = ['--testPathPattern=test/test-main.ts', '--forceExit', '--detectOpenHandles']; - -if (args.includes('--update')) { - process.env.UPDATE_SNAPSHOTS = 'true'; -} - -if (args.includes('--verbose')) { - process.env.VERBOSE = 'true'; -} - -// Run Jest programmatically -jest.run(jestArgs); From 36a0722b44e4df9c98115489d0895a29b6272eb9 Mon Sep 17 00:00:00 2001 From: jupblb Date: Fri, 15 Aug 2025 15:23:21 +0200 Subject: [PATCH 6/6] style: Format Jest config files with Prettier --- packages/pyright-scip/jest.config.js | 9 +++++--- packages/pyright-scip/jest.snapshot.config.js | 22 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/pyright-scip/jest.config.js b/packages/pyright-scip/jest.config.js index 4228adf34..c379ee96b 100644 --- a/packages/pyright-scip/jest.config.js +++ b/packages/pyright-scip/jest.config.js @@ -20,10 +20,13 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleNameMapper: { ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), - '^typescript-char$': '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', + '^typescript-char$': + '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', '^vscode-uri$': '/../pyright-internal/node_modules/.pnpm/vscode-uri@3.1.0/node_modules/vscode-uri', - '^vscode-languageserver-protocol$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', - '^vscode-languageserver-types$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', + '^vscode-languageserver-protocol$': + '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', + '^vscode-languageserver-types$': + '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', }, globals: { 'ts-jest': { diff --git a/packages/pyright-scip/jest.snapshot.config.js b/packages/pyright-scip/jest.snapshot.config.js index 992475981..0e12e8ec3 100644 --- a/packages/pyright-scip/jest.snapshot.config.js +++ b/packages/pyright-scip/jest.snapshot.config.js @@ -11,19 +11,25 @@ module.exports = { testEnvironment: 'node', roots: ['/test/'], transform: { - '^.+\\.tsx?$': ['ts-jest', { - tsconfig: 'tsconfig.test.json', - isolatedModules: true, - diagnostics: false, - }], + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.test.json', + isolatedModules: true, + diagnostics: false, + }, + ], }, testMatch: ['**/test/test-*.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleNameMapper: { ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), - '^typescript-char$': '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', + '^typescript-char$': + '/../pyright-internal/node_modules/.pnpm/typescript-char@0.0.0/node_modules/typescript-char', '^vscode-uri$': '/../pyright-internal/node_modules/.pnpm/vscode-uri@3.1.0/node_modules/vscode-uri', - '^vscode-languageserver-protocol$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', - '^vscode-languageserver-types$': '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', + '^vscode-languageserver-protocol$': + '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-protocol@3.17.3/node_modules/vscode-languageserver-protocol', + '^vscode-languageserver-types$': + '/../pyright-internal/node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types', }, };