diff --git a/qt-cpp/package.json b/qt-cpp/package.json index 39def703..fb7e78cf 100644 --- a/qt-cpp/package.json +++ b/qt-cpp/package.json @@ -278,14 +278,17 @@ "tsc-test": "npm run compile && npm run build-test && cd ./out/test/ && node runTest.js", "prepare-vsix": "npm --prefix ../qt-core run package", "run-test-build": "cd ./out/test/ && node runTest.build.js", + "run-test-presets": "cd ./out/test/ && node runTest.presets.js", "natvis:golden:update": "npm run compile-test && cd ./out/test/ && cross-env UPDATE_NATVIS_GOLDEN=1 QT_TEST_QT_ROOT=${npm_config_qt_root:-$QT_TEST_QT_ROOT} node runTest.natvis.js", "run-test-natvis": "cd ./out/test/ && node runTest.natvis.js", "run-test": "cd ./out/test/ && node runTest.js", "test:only:natvis": "npm run compile-test && npm run run-test-natvis --", "test:only:build": "npm run compile-test && npm run run-test-build --", + "test:only:presets": "npm run compile-test && npm run run-test-presets --", "test:only": "npm run compile-test && npm run run-test && npm run run-test-build && npm run run-test-natvis", "run-tests": "cd ./out/test/ && QT_TEST_QT_ROOT=${npm_config_qt_root:-$QT_TEST_QT_ROOT} node runTest.js && QT_TEST_QT_ROOT=${npm_config_qt_root:-$QT_TEST_QT_ROOT} node runTest.build.js && QT_TEST_QT_ROOT=${npm_config_qt_root:-$QT_TEST_QT_ROOT} node runTest.natvis.js", "test:build": "npm run prepare-vsix && npm run test:only:build --", + "test:presets": "npm run prepare-vsix && npm run test:only:presets --", "test:natvis": "npm run prepare-vsix && npm run test:only:natvis --", "test": "npm run prepare-vsix && npm run compile-test && npm run run-tests", "lint": "npm run prettierWrite && eslint . --fix --cache", diff --git a/qt-cpp/test/helper.mts b/qt-cpp/test/helper.mts index 8ba630e9..0a967692 100644 --- a/qt-cpp/test/helper.mts +++ b/qt-cpp/test/helper.mts @@ -299,13 +299,48 @@ export async function cleanBuildDir( return buildDir; } -export async function setCMakeGeneratorForPlatform( - ws: vscode.WorkspaceFolder -): Promise { - const generator = process.platform === 'win32' ? 'Ninja' : 'Unix Makefiles'; - await vscode.workspace - .getConfiguration('cmake', ws.uri) - .update('generator', generator, vscode.ConfigurationTarget.Workspace); +class CMakeConfigrator { + private ws: vscode.WorkspaceFolder; + private resetValues: Map = new Map(); + + constructor(ws: vscode.WorkspaceFolder) { + this.ws = ws; + } + + async set( + settingName: string, + value: unknown, + resetValue?: unknown + ): Promise { + if (!this.resetValues.has(settingName)) { + this.resetValues.set(settingName, resetValue); + } + await vscode.workspace + .getConfiguration('cmake', this.ws.uri) + .update(settingName, value, vscode.ConfigurationTarget.Workspace); + } + resetAll() { + const resets: Promise[] = []; + for (const [settingName, value] of this.resetValues.entries()) { + resets.push( + Promise.resolve( + vscode.workspace + .getConfiguration('cmake', this.ws.uri) + .update(settingName, value, vscode.ConfigurationTarget.Workspace) + ) + ); + } + this.resetValues.clear(); + return Promise.all(resets); + } +} + +export function cmakeConfigForWorkspace(ws: vscode.WorkspaceFolder) { + return new CMakeConfigrator(ws); +} + +export function getPlatformCMakeGenerator(): string { + return process.platform === 'win32' ? 'Ninja' : 'Unix Makefiles'; } /** diff --git a/qt-cpp/test/runTest.build.mts b/qt-cpp/test/runTest.build.mts index 5c6dcf1a..4b58a674 100644 --- a/qt-cpp/test/runTest.build.mts +++ b/qt-cpp/test/runTest.build.mts @@ -31,7 +31,11 @@ async function main() { setupVSCodeSettings(userDataDir, qtRoot, { 'cmake.configureOnOpen': false }); - installRequiredExtensions(cli, args, localQtCoreVsix); + const extensions = [ + { idOrVsix: 'ms-vscode.cmake-tools' }, + { idOrVsix: localQtCoreVsix } + ]; + installRequiredExtensions(cli, args, extensions); // The workspace folder we want to open const projectDir = path.resolve(__dirname, '../../test/projectFolder'); diff --git a/qt-cpp/test/runTest.mts b/qt-cpp/test/runTest.mts index 243a5c81..6077cfdb 100644 --- a/qt-cpp/test/runTest.mts +++ b/qt-cpp/test/runTest.mts @@ -27,7 +27,11 @@ async function main() { await setupTestInfrastructure(vscodeExecutablePath); setupVSCodeSettings(userDataDir, qtRoot); - installRequiredExtensions(cli, args, localQtCoreVsix); + const extensions = [ + { idOrVsix: 'ms-vscode.cmake-tools' }, + { idOrVsix: localQtCoreVsix } + ]; + installRequiredExtensions(cli, args, extensions); // Run the integration tests (no need to pass launchArgs; we reused the same dirs) await runTests({ diff --git a/qt-cpp/test/runTest.natvis.mts b/qt-cpp/test/runTest.natvis.mts index 77f3f499..a590dbe3 100644 --- a/qt-cpp/test/runTest.natvis.mts +++ b/qt-cpp/test/runTest.natvis.mts @@ -7,14 +7,6 @@ import * as fsp from 'fs/promises'; import { downloadAndUnzipVSCode, runTests } from '@vscode/test-electron'; -import { getQuietVSCodeArgs } from '../../qt-lib/src/test-constants.js'; - -import { - installExtensionWithRetry, - debugListExtensions, - assertExtensionsInstalled -} from '../../qt-lib/src/test-vscode-install.js'; - import { setupVSCodeSettings, setupTestInfrastructure, @@ -46,14 +38,12 @@ async function main() { }); // Install core required extensions (CMake Tools + qt-core) via helper - installRequiredExtensions(cli, args, localQtCoreVsix); - - const quietArgs = [...args, ...getQuietVSCodeArgs()]; - installExtensionWithRetry(cli, quietArgs, 'ms-vscode.cpptools'); - - // Final extension check: ensure cpptools is present - debugListExtensions(cli, quietArgs); - assertExtensionsInstalled(cli, quietArgs, ['ms-vscode.cpptools']); + const extensions = [ + { idOrVsix: 'ms-vscode.cmake-tools' }, + { idOrVsix: 'ms-vscode.cpptools' }, + { idOrVsix: localQtCoreVsix } + ]; + installRequiredExtensions(cli, args, extensions); // The workspace folder we want to open const projectDir = path.resolve( diff --git a/qt-cpp/test/runTest.presets.mts b/qt-cpp/test/runTest.presets.mts new file mode 100644 index 00000000..ab65a651 --- /dev/null +++ b/qt-cpp/test/runTest.presets.mts @@ -0,0 +1,70 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import * as path from 'path'; +import * as os from 'os'; +import * as fsp from 'fs/promises'; + +import { downloadAndUnzipVSCode, runTests } from '@vscode/test-electron'; + +import { + setupTestInfrastructure, + setupVSCodeSettings, + installRequiredExtensions +} from './runTestHelper.mjs'; +import { ExtensionInstallInfo } from 'qt-lib/src/test-vscode-install.ts'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to --extensionDevelopmentPath + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index-presets'); + + const vscodeExecutablePath = await downloadAndUnzipVSCode(); + + const { qtRoot, localQtCoreVsix, cli, args, userDataDir } = + await setupTestInfrastructure(vscodeExecutablePath); + + setupVSCodeSettings(userDataDir, qtRoot, { + 'cmake.configureOnOpen': false + }); + const extensions: ExtensionInstallInfo[] = [ + { idOrVsix: 'ms-vscode.cmake-tools', preRelease: true }, + { idOrVsix: localQtCoreVsix } + ]; + installRequiredExtensions(cli, args, extensions); + + // The workspace folder we want to open + const projectDir = path.resolve(__dirname, '../../test/projectFolder'); + console.log('[runTest] Using project dir:', projectDir); + const tmpWs = await fsp.mkdtemp(path.join(os.tmpdir(), 'qt-cpp-ws-')); + const tmpProject = path.join(tmpWs, 'project'); + await fsp.mkdir(tmpProject, { recursive: true }); + // Node 16+: recursive copy + await fsp.cp(projectDir, tmpProject, { recursive: true }); + console.log('[runTest] Copied project to temp dir:', tmpProject); + + // Run the integration tests (no need to pass launchArgs; we reused the same dirs) + try { + await runTests({ + launchArgs: [tmpProject, '--disable-workspace-trust'], + extensionDevelopmentPath, + extensionTestsPath + }); + } finally { + try { + await fsp.rm(tmpWs, { recursive: true, force: true }); + } catch {} + } + } catch (e: Error | unknown) { + console.error('Failed to run tests'); + console.error(e); + process.exit(1); + } +} + +main(); diff --git a/qt-cpp/test/runTestHelper.mts b/qt-cpp/test/runTestHelper.mts index 6ae95820..db566ffb 100644 --- a/qt-cpp/test/runTestHelper.mts +++ b/qt-cpp/test/runTestHelper.mts @@ -13,7 +13,8 @@ import { installExtensionWithRetry, debugListExtensions, assertExtensionsInstalled, - getDebugLevel + getDebugLevel, + ExtensionInstallInfo } from '../../qt-lib/src/test-vscode-install.js'; const QT_INS_ROOT_CONFIG_NAME = 'qtInstallationRoot'; @@ -108,18 +109,17 @@ export function setupVSCodeSettings( export function installRequiredExtensions( cli: string, args: string[], - localQtCoreVsix: string + extensions: ExtensionInstallInfo[], + requiredIDs: string[] = ['ms-vscode.cmake-tools', 'theqtcompany.qt-core'] ): void { const quietArgs = [...args, ...getQuietVSCodeArgs()]; - const required = ['ms-vscode.cmake-tools', localQtCoreVsix]; - const requiredIds = ['ms-vscode.cmake-tools', 'theqtcompany.qt-core']; - for (const ext of required) { + for (const ext of extensions) { installExtensionWithRetry(cli, quietArgs, ext); } debugListExtensions(cli, args); - assertExtensionsInstalled(cli, args, requiredIds); + assertExtensionsInstalled(cli, args, requiredIDs); } /** diff --git a/qt-cpp/test/suite/build.test.mts b/qt-cpp/test/suite/build.test.mts index a6cac81c..734f7e87 100644 --- a/qt-cpp/test/suite/build.test.mts +++ b/qt-cpp/test/suite/build.test.mts @@ -15,7 +15,8 @@ import { prepareCMakeQtEnvWithVersion, getWorkspaceFolderOrThrow, cleanBuildDir, - setCMakeGeneratorForPlatform, + cmakeConfigForWorkspace, + getPlatformCMakeGenerator, prepareStandardCMakeArgs, readCMakeCacheVar, selectAndApplyKit @@ -33,11 +34,12 @@ describe('build: minimal Qt project (index-build)', function () { it('configures and builds a tiny Qt app', async function () { const wsFolder = getWorkspaceFolderOrThrow(); + const cmakeConfigurator = cmakeConfigForWorkspace(wsFolder); const projectDir = wsFolder.uri.fsPath; console.log('Using projectDir:', projectDir); const buildDir = await cleanBuildDir(projectDir); - await setCMakeGeneratorForPlatform(wsFolder); + await cmakeConfigurator.set('generator', getPlatformCMakeGenerator()); await selectAndApplyKit(); @@ -85,5 +87,7 @@ describe('build: minimal Qt project (index-build)', function () { expect(fs.existsSync(outPath), `Expected build artifact at ${outPath}`).to .be.true; expect(errSpy.called, 'Unexpected error popups during build').to.be.false; + + await cmakeConfigurator.resetAll(); }); }); diff --git a/qt-cpp/test/suite/index-presets.mts b/qt-cpp/test/suite/index-presets.mts new file mode 100644 index 00000000..cdf3b4eb --- /dev/null +++ b/qt-cpp/test/suite/index-presets.mts @@ -0,0 +1,33 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +// This index is used exclusively for CMake Presets tests. +// It should only include presets.test.mts to test CMake Presets functionality +// with the prerelease version of CMake Tools. + +import * as path from 'path'; +import Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + const mocha = new Mocha({ ui: 'bdd', color: true }); + + const testsRoot = path.resolve(__dirname); + + return new Promise((resolve, reject) => { + // Only include the presets test file that esbuild outputs + const g = new glob.Glob('presets.test.js', { cwd: testsRoot }); + + g.stream() + .on('data', (file) => mocha.addFile(path.resolve(testsRoot, file))) + .on('error', (err) => + reject(err instanceof Error ? err : new Error(String(err))) + ) + .on('end', () => { + mocha.timeout(150_000); + mocha.run((failures) => + failures ? reject(new Error(`${failures} tests failed.`)) : resolve() + ); + }); + }); +} diff --git a/qt-cpp/test/suite/index.mts b/qt-cpp/test/suite/index.mts index d14a0818..efc07dfd 100644 --- a/qt-cpp/test/suite/index.mts +++ b/qt-cpp/test/suite/index.mts @@ -16,7 +16,7 @@ export function run(): Promise { return new Promise((c, e) => { const testFiles = new glob.Glob('**.test.js', { cwd: testsRoot, - ignore: ['**/build.test.js', '**/natvis.test.js'] + ignore: ['**/build.test.js', '**/natvis.test.js', '**/presets.test.js'] }); const testFileStream = testFiles.stream(); testFileStream.on('data', (file) => { diff --git a/qt-cpp/test/suite/presets.test.mts b/qt-cpp/test/suite/presets.test.mts new file mode 100644 index 00000000..50e6b8a4 --- /dev/null +++ b/qt-cpp/test/suite/presets.test.mts @@ -0,0 +1,173 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { + delay, + getCoreApi, + CoreKey, + findQtPathsInInstallationPath +} from 'qt-lib'; +import { + setupSandboxLifecycleHooks, + waitForVSCodeIdle, + activateQtCpp, + prepareCMakeQtEnvWithVersion, + getWorkspaceFolderOrThrow, + cleanBuildDir, + readCMakeCacheVar, + cmakeConfigForWorkspace +} from '../helper.mts'; + +// Test timing constants +const FS_SETTLE_DELAY_MS = 500; // Wait for file system to settle after writing files +const DISK_FLUSH_DELAY_MS = 400; // Wait for build artifacts to flush to disk + +describe('presets: CMake Presets integration', function () { + this.timeout(150_000); + + let sb: sinon.SinonSandbox; + + setupSandboxLifecycleHooks( + (_sb) => (sb = _sb), + async () => activateQtCpp() + ); + + it('configures and builds a tiny Qt app with CMake Presets', async function () { + const wsFolder = getWorkspaceFolderOrThrow(); + const cmakeConfigurator = cmakeConfigForWorkspace(wsFolder); + await cmakeConfigurator.set('useCMakePresets', 'always'); + await waitForVSCodeIdle(); + const projectDir = wsFolder.uri.fsPath; + const buildDir = await cleanBuildDir(projectDir, 'build-presets'); + const qtRoot = vscode.workspace + .getConfiguration('qt-core') + .get('qtInstallationRoot'); + if (typeof qtRoot !== 'string' || qtRoot.trim() === '') { + throw new Error('qt-core.qtInstallationRoot is not configured.'); + } + const qtEnv = prepareCMakeQtEnvWithVersion({ + topLevel: qtRoot, + verbose: true + }); + const presetsPath = path.join(projectDir, 'CMakePresets.json'); + + // Create a CMake Presets configuration + // Note: generator is omitted because CMake will use the default generator + // or the one specified in CMake Tools settings + const presets = { + version: 3, + configurePresets: [ + { + name: 'qt-debug', + displayName: 'Qt Debug Configuration', + description: 'Debug build using Qt with CMake Presets', + binaryDir: buildDir, + cacheVariables: { + CMAKE_BUILD_TYPE: 'Debug', + CMAKE_PREFIX_PATH: qtEnv.leaf + } + } + ] + }; + + fs.writeFileSync(presetsPath, JSON.stringify(presets, null, 2), 'utf-8'); + console.log( + 'Created/Updated CMakePresets.json with CMAKE_PREFIX_PATH:', + qtEnv.leaf + ); + console.log('Using projectDir (Presets):', projectDir); + + // Wait for file system to settle after writing CMakePresets.json + await delay(FS_SETTLE_DELAY_MS); + await waitForVSCodeIdle(); + + // Disable automatic configuration to have precise control over test flow + // configureOnOpen would trigger configure before we can set the preset + // automaticReconfigure would interfere with our explicit configure call + await cmakeConfigurator.set('configureOnOpen', false); + await cmakeConfigurator.set('automaticReconfigure', false); + + // spy on error messages + const errSpy = sb.spy(vscode.window, 'showErrorMessage'); + + // Set the configure preset using the correct CMake Tools command + console.log('Setting configure preset: qt-debug'); + await vscode.commands.executeCommand( + 'cmake.setConfigurePreset', + 'qt-debug' + ); + await waitForVSCodeIdle(); + + console.log('Running cmake.configure with presets...'); + const rcCfg = + await vscode.commands.executeCommand('cmake.configure'); + await waitForVSCodeIdle(); + expect(rcCfg, `cmake.configure failed (rc=${rcCfg})`).to.equal(0); + + // Confirm what CMake used + if (process.env.QT_TEST_DEBUG === '1') { + console.log('== WHAT CMAKE USED (Presets) =='); + console.log( + ' CMAKE_PREFIX_PATH =', + readCMakeCacheVar(buildDir, 'CMAKE_PREFIX_PATH') ?? '' + ); + } + + // Build + const rcBuild = await vscode.commands.executeCommand('cmake.build'); + await waitForVSCodeIdle(); + expect(rcBuild, `cmake.build failed (rc=${rcBuild})`).to.equal(0); + + // Wait for build artifacts to be written to disk + await delay(DISK_FLUSH_DELAY_MS); + + const bin = + process.platform === 'win32' ? path.join('Debug', 'hello.exe') : 'hello'; + const outPath = path.join(buildDir, bin); + console.log('Checking for binary at', outPath); + + expect(fs.existsSync(outPath), `Expected build artifact at ${outPath}`).to + .be.true; + expect(errSpy.called, 'Unexpected error popups during build').to.be.false; + + // Verify that SELECTED_QT_PATHS is set correctly in CoreAPI + const coreAPI = await getCoreApi(); + if (!coreAPI) { + throw new Error('CoreAPI is not available'); + } + + const selectedQtPaths = coreAPI.getValue( + wsFolder, + CoreKey.SELECTED_QT_PATHS + ); + console.log('CoreAPI SELECTED_QT_PATHS:', selectedQtPaths); + + // Get expected qtpaths from the Qt installation + const expectedQtPaths = findQtPathsInInstallationPath(qtEnv.leaf); + console.log('Expected qtpaths from installation:', expectedQtPaths); + + expect(selectedQtPaths, 'SELECTED_QT_PATHS should be set in CoreAPI').to.not + .be.empty; + expect( + selectedQtPaths, + 'SELECTED_QT_PATHS should match the qtpaths executable in the Qt installation' + ).to.equal(expectedQtPaths); + + // Cleanup: remove CMakePresets.json and reset useCMakePresets + try { + if (fs.existsSync(presetsPath)) { + fs.unlinkSync(presetsPath); + } + await cmakeConfigurator.resetAll(); + await waitForVSCodeIdle(); + } catch (e) { + console.warn('Cleanup warning:', e); + } + }); +}); diff --git a/qt-lib/src/test-vscode-install.ts b/qt-lib/src/test-vscode-install.ts index 8e17c718..daf2baf3 100644 --- a/qt-lib/src/test-vscode-install.ts +++ b/qt-lib/src/test-vscode-install.ts @@ -3,6 +3,11 @@ import * as cp from 'child_process'; +export interface ExtensionInstallInfo { + idOrVsix: string; + preRelease?: boolean; +} + export function parseVSCodeDirs(cliArgs: string[]) { const pick = (key: string) => { const pref = `${key}=`; @@ -18,24 +23,23 @@ export function parseVSCodeDirs(cliArgs: string[]) { export function installExtensionWithRetry( cli: string, baseArgs: string[], - idOrVsix: string, + ext: ExtensionInstallInfo, attempts = 3 ) { for (let i = 1; i <= attempts; i++) { const cleanEnv = { ...process.env }; delete (cleanEnv as Record).ELECTRON_RUN_AS_NODE; - - const res = cp.spawnSync( - cli, - [...baseArgs, '--install-extension', idOrVsix, '--force'], - { - encoding: 'utf-8', - // Keep output quiet unless you want to debug: - stdio: process.env.VS_LOG_VERBOSE ? 'inherit' : 'pipe', - shell: process.platform === 'win32', - env: cleanEnv - } - ); + const args = [...baseArgs, '--install-extension', ext.idOrVsix, '--force']; + if (ext.preRelease) { + args.push('--pre-release'); + } + const res = cp.spawnSync(cli, args, { + encoding: 'utf-8', + // Keep output quiet unless you want to debug: + stdio: process.env.VS_LOG_VERBOSE ? 'inherit' : 'pipe', + shell: process.platform === 'win32', + env: cleanEnv + }); if (res.status === 0) { return; } @@ -48,7 +52,7 @@ export function installExtensionWithRetry( console.error(res.stderr); } console.error( - `[runTest] install "${idOrVsix}" failed (attempt ${i}/${attempts})` + `[runTest] install "${ext.idOrVsix}" failed (attempt ${i}/${attempts})` ); if (i < attempts) { try { @@ -61,7 +65,7 @@ export function installExtensionWithRetry( } } } - console.error(`[runTest] Giving up installing "${idOrVsix}"`); + console.error(`[runTest] Giving up installing "${ext.idOrVsix}"`); process.exit(1); } diff --git a/qt-qml/test/runTest.ts b/qt-qml/test/runTest.ts index 7bd7bc18..daf414b0 100644 --- a/qt-qml/test/runTest.ts +++ b/qt-qml/test/runTest.ts @@ -54,7 +54,9 @@ async function main() { const quietArgs = [...args, ...getQuietVSCodeArgs()]; const requiredIds = ['theqtcompany.qt-core']; // Install required extensions into the SAME profile/dir combo - installExtensionWithRetry(cli as string, quietArgs, localQtCoreVsix); + installExtensionWithRetry(cli as string, quietArgs, { + idOrVsix: localQtCoreVsix + }); debugListExtensions(cli as string, args); assertExtensionsInstalled(cli as string, args, requiredIds); diff --git a/qt-ui/test/runTest.ts b/qt-ui/test/runTest.ts index c831a8c3..536bef0a 100644 --- a/qt-ui/test/runTest.ts +++ b/qt-ui/test/runTest.ts @@ -120,7 +120,9 @@ async function main() { const quietArgs = [...args, ...getQuietVSCodeArgs()]; const requiredIds = ['theqtcompany.qt-core']; // Install required extensions into the SAME profile/dir combo - installExtensionWithRetry(cli as string, quietArgs, localQtCoreVsix); + installExtensionWithRetry(cli as string, quietArgs, { + idOrVsix: localQtCoreVsix + }); debugListExtensions(cli as string, args); assertExtensionsInstalled(cli as string, args, requiredIds);