Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
446a3ea
Revert .NET 10 custom code support
lambrianmsft May 14, 2026
aac035f
Restore coverage for .NET 10 revert
lambrianmsft May 14, 2026
858ebca
Increase PR coverage heap for VS Code tests
lambrianmsft May 14, 2026
ff7a896
Make codeful workflow test paths cross-platform
lambrianmsft May 14, 2026
18987af
Stabilize hotfix PR CI tests
lambrianmsft May 14, 2026
78d58ed
Reduce VS Code designer coverage memory
lambrianmsft May 14, 2026
3bcb03a
Stabilize VS Code designer unit test heap
lambrianmsft May 14, 2026
9f35cdd
Limit VS Code designer PR coverage scope
lambrianmsft May 14, 2026
f5e3eb3
Use PR base SHA for VS Code coverage scope
lambrianmsft May 14, 2026
1309929
Pass PR coverage env through Turbo
lambrianmsft May 14, 2026
3a2f7da
Write VS Code coverage scope before Turbo tests
lambrianmsft May 15, 2026
9ca713f
Use V8 coverage for VS Code designer tests
lambrianmsft May 15, 2026
385cee8
Propagate heap size to VS Code test workers
lambrianmsft May 15, 2026
9f96bac
Split VS Code designer coverage from full unit run
lambrianmsft May 15, 2026
d86c991
Exclude E2E harness from VS Code unit shards
lambrianmsft May 15, 2026
16a4a41
Run VS Code unit shards one file at a time
lambrianmsft May 15, 2026
018560a
Minimize .NET 10 revert hotfix
lambrianmsft May 26, 2026
eb2a2a0
Stabilize Net8 csproj assertion
lambrianmsft May 26, 2026
5f674dc
Increase CI heap for extension tests
lambrianmsft May 26, 2026
844365f
Scope VS Code coverage in CI
lambrianmsft May 26, 2026
b270cf4
Isolate VS Code designer unit tests
lambrianmsft May 26, 2026
6a4ee27
Remove redundant CreateFunctionAppFiles test
lambrianmsft May 26, 2026
4dc61fa
Wait for workflow runtime in VS Code E2E
lambrianmsft May 27, 2026
156bd82
Make workflow runtime probe best effort
lambrianmsft May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/pr-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,27 @@ jobs:
- name: Build
run: pnpm turbo run build --cache-dir=.turbo

- name: Configure VS Code designer coverage scope
run: |
CHANGED_DESIGNER_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} "$GITHUB_SHA" -- 'apps/vs-code-designer/src/**/*.ts' 'apps/vs-code-designer/src/**/*.tsx' \
| grep -Ev 'apps/vs-code-designer/src/test/(e2e|ui)/|\.test\.tsx?$|\.spec\.tsx?$|test-setup\.ts|extensionVariables\.ts|main\.ts' \
| sed 's#^apps/vs-code-designer/##' \
| paste -sd, -)
if [ -n "$CHANGED_DESIGNER_FILES" ]; then
echo "VSCODE_DESIGNER_COVERAGE_INCLUDE=$CHANGED_DESIGNER_FILES" >> "$GITHUB_ENV"
echo "$CHANGED_DESIGNER_FILES"
else
echo "VSCODE_DESIGNER_COVERAGE_INCLUDE=__no_changed_source__/**" >> "$GITHUB_ENV"
echo "No changed VS Code designer source files require coverage."
fi

- name: Run tests with coverage
run: |
pnpm turbo run test:lib --cache-dir=.turbo
pnpm turbo run test:iframe-app --cache-dir=.turbo
pnpm turbo run test:extension-unit --cache-dir=.turbo
env:
NODE_OPTIONS: --max-old-space-size=8192

- name: Merge coverage reports
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ jobs:
- run: pnpm turbo run test:lib --cache-dir=.turbo
- run: pnpm turbo run build:extension --cache-dir=.turbo
- run: pnpm turbo run test:extension-unit --cache-dir=.turbo
env:
NODE_OPTIONS: --max-old-space-size=8192
VITEST_COVERAGE: false
- run: pnpm turbo run test:iframe-app --cache-dir=.turbo
4 changes: 0 additions & 4 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,6 @@
"JqiwYx": "Review + create",
"JrAqnE": "Run with payload",
"JrDiMJ": "Package path cannot be empty",
"JsTRX9": ".NET 10",
"JsUu6b": "Workflow",
"JyYLq1": "Zoom out",
"JzRzVp": "(UTC-09:00) Alaska",
Expand Down Expand Up @@ -1410,7 +1409,6 @@
"Q0xpPQ": "Required. The object to check if it is less or equal to the comparing object.",
"Q13J5V": "Create deployment model",
"Q1LEiE": "Previous",
"Q1tyGI": "Use the latest .NET 10 for modern development and performance",
"Q2X3qQ": "Actions need to be triggered by another node, e.g. at regular intervals with the Schedule node",
"Q2p4Zh": "An error occurred while generating keys. Error details: {errorMessage}",
"Q4TUFX": "Discard",
Expand Down Expand Up @@ -3042,7 +3040,6 @@
"_JqiwYx.comment": "Review and create step title",
"_JrAqnE.comment": "Tooltip for Run with payload button",
"_JrDiMJ.comment": "Package path empty validation message",
"_JsTRX9.comment": ".NET 10 option",
"_JsUu6b.comment": "Label for workflow template which contains single workflow",
"_JyYLq1.comment": "Aria label for a button that zooms out on the workflow",
"_JzRzVp.comment": "Time zone value ",
Expand Down Expand Up @@ -3347,7 +3344,6 @@
"_Q0xpPQ.comment": "Required object parameter to check if less than or equal to using lessOrEqual function",
"_Q13J5V.comment": "Create deployment model resource label",
"_Q1LEiE.comment": "Button text for going back to the previous tab",
"_Q1tyGI.comment": ".NET 10 description",
"_Q2X3qQ.comment": "Message explaining that actions need triggers",
"_Q2p4Zh.comment": "General error message for key generation failure",
"_Q4TUFX.comment": "Button text for discard the unsaved changes",
Expand Down
2 changes: 1 addition & 1 deletion apps/vs-code-designer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"vscode:designer:pack:step1": "cd ./dist && npm install",
"vscode:designer:pack:step2": "cd ./dist && vsce package",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"test:extension-unit": "vitest run --retry=3",
"test:extension-unit": "node ./scripts/run-vitest-isolated.mjs --retry=3",
"test:e2e-cli": "vscode-test",
"test:e2e-cli:compile": "tsc -p ./tsconfig.e2e.json",
"pretest:e2e-cli": "pnpm run test:e2e-cli:compile",
Expand Down
112 changes: 112 additions & 0 deletions apps/vs-code-designer/scripts/run-vitest-isolated.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { spawn } from 'node:child_process';
import { existsSync, mkdirSync, readdirSync, rmSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const vitestPath = fileURLToPath(new URL('../../../node_modules/vitest/vitest.mjs', import.meta.url));
const args = process.argv.slice(2);
const coverageEnabled = process.env.VITEST_COVERAGE !== 'false';
const coverageDir = join(process.cwd(), 'coverage');
const shardCoverageDir = join(process.cwd(), '.vitest-coverage-shards');

const isTestFile = (file) => /(^|[/\\])src[/\\].*\.test\.tsx?$/.test(file);
const isExcludedTestHarness = (file) => file.startsWith('src/test/');
const fileArgs = args.filter(isTestFile).map((file) => file.replace(/\\/g, '/'));
const baseArgs = args.filter((arg) => !isTestFile(arg));

const findTestFiles = (directory) => {
const files = [];

for (const entry of readdirSync(directory, { withFileTypes: true })) {
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...findTestFiles(entryPath));
} else if (/\.test\.tsx?$/.test(entry.name)) {
const testFile = relative(process.cwd(), entryPath).replace(/\\/g, '/');
if (!isExcludedTestHarness(testFile)) {
files.push(testFile);
}
}
}

return files;
};

const runVitest = (testFile) =>
new Promise((resolve) => {
const child = spawn(process.execPath, [vitestPath, 'run', ...baseArgs, testFile], {
env: process.env,
stdio: 'inherit',
});

child.on('exit', (code, signal) => {
resolve({ code: code ?? 1, signal });
});
});

const saveCoverage = (index) => {
const lcovPath = join(coverageDir, 'lcov.info');
if (existsSync(lcovPath)) {
mkdirSync(shardCoverageDir, { recursive: true });
copyFileSync(lcovPath, join(shardCoverageDir, `lcov-${String(index).padStart(4, '0')}.info`));
}
};

const mergeCoverage = () => {
if (!existsSync(shardCoverageDir)) {
return;
}

const lcovFiles = readdirSync(shardCoverageDir)
.filter((file) => file.endsWith('.info'))
.sort();

if (lcovFiles.length === 0) {
return;
}

mkdirSync(coverageDir, { recursive: true });
const merged = lcovFiles
.map((file) => readFileSync(join(shardCoverageDir, file), 'utf8').trim())
.filter(Boolean)
.join('\n');
writeFileSync(join(coverageDir, 'lcov.info'), `${merged}\n`);
};

const run = async () => {
const testFiles = (fileArgs.length ? fileArgs : findTestFiles(join(process.cwd(), 'src')).sort()).filter(
(file) => !isExcludedTestHarness(file)
);

if (coverageEnabled) {
rmSync(shardCoverageDir, { recursive: true, force: true });
}

for (const [index, testFile] of testFiles.entries()) {
console.log(`Running VS Code designer unit test ${index + 1}/${testFiles.length}: ${testFile}`);
if (coverageEnabled) {
rmSync(coverageDir, { recursive: true, force: true });
}

const result = await runVitest(testFile);

if (result.signal) {
process.kill(process.pid, result.signal);
return;
}

if (result.code !== 0) {
process.exit(result.code);
}

if (coverageEnabled) {
saveCoverage(index);
}
}

if (coverageEnabled) {
mergeCoverage();
}
};

run();
Original file line number Diff line number Diff line change
@@ -1,48 +1,110 @@
import { describe, it, expect, vi } from 'vitest';
import { TargetFramework } from '@microsoft/vscode-extension-logic-apps';
import * as fs from 'fs-extra';
import { window } from 'vscode';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ext } from '../../../../../extensionVariables';
import { FunctionFileStep } from '../functionFileStep';

vi.mock('fs-extra', () => ({
pathExists: vi.fn(),
stat: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
}));

vi.mock('vscode', () => ({
window: {
showErrorMessage: vi.fn(),
},
}));

vi.mock('fs-extra');
vi.mock('vscode');
vi.mock('../../../../../extensionVariables', () => ({
ext: { outputChannel: { appendLog: vi.fn() } },
ext: {
outputChannel: {
appendLog: vi.fn(),
},
},
}));

vi.mock('../../../../../localize', () => ({
localize: (_key: string, msg: string) => msg,
localize: (_key: string, message: string) => message,
}));

import { FunctionFileStep } from '../functionFileStep';

describe('FunctionFileStep', () => {
describe('csTemplateFileName mapping', () => {
it('should map Net10 to FunctionsFileNet10', () => {
const step = new FunctionFileStep();
const mapping = (step as any).csTemplateFileName;
expect(mapping[TargetFramework.Net10]).toBe('FunctionsFileNet10');
});

it('should preserve Net8 mapping', () => {
const step = new FunctionFileStep();
const mapping = (step as any).csTemplateFileName;
expect(mapping[TargetFramework.Net8]).toBe('FunctionsFileNet8');
});

it('should preserve NetFx mapping', () => {
const step = new FunctionFileStep();
const mapping = (step as any).csTemplateFileName;
expect(mapping[TargetFramework.NetFx]).toBe('FunctionsFileNetFx');
});

it('should contain exactly three framework entries', () => {
const step = new FunctionFileStep();
const mapping = (step as any).csTemplateFileName;
expect(Object.keys(mapping)).toHaveLength(3);
});
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.pathExists).mockResolvedValue(true);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.mocked(fs.readFile).mockResolvedValue('namespace <%= namespace %> { public class <%= methodName %> {} }');
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
});

describe('shouldPrompt', () => {
it('should always return true', () => {
const step = new FunctionFileStep();
expect(step.shouldPrompt()).toBe(true);
});
it('should always prompt', () => {
expect(new FunctionFileStep().shouldPrompt()).toBe(true);
});

it('should log and return when the functions folder is missing', async () => {
vi.mocked(fs.pathExists).mockResolvedValue(false);
const context = createContext();

await new FunctionFileStep().prompt(context);

expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('is not a valid directory'));
expect(fs.writeFile).not.toHaveBeenCalled();
});

it('should log and return when the functions path is not a directory', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as any);
const context = createContext();

await new FunctionFileStep().prompt(context);

expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('is not a valid directory'));
expect(fs.writeFile).not.toHaveBeenCalled();
});

it('should create a .NET 8 function file from the template', async () => {
vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
const context = createContext();

await new FunctionFileStep().prompt(context);

expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNet8'), 'utf-8');
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('ProcessOrder.cs'),
'namespace Contoso.Functions { public class ProcessOrder {} }'
);
});

it('should create a .NET Framework function file from the template', async () => {
vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
const context = createContext({ targetFramework: TargetFramework.NetFx });

await new FunctionFileStep().prompt(context);

expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNetFx'), 'utf-8');
expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('ProcessOrder.cs'), expect.any(String));
});

it('should not overwrite an existing function file', async () => {
vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(true);
const context = createContext();

await new FunctionFileStep().prompt(context);

expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('already exists'));
expect(window.showErrorMessage).toHaveBeenCalledWith(expect.stringContaining('already exists in the target functions project'));
expect(fs.writeFile).not.toHaveBeenCalled();
});
});

function createContext(overrides: Record<string, unknown> = {}) {
return {
workspacePath: 'C:\\workspace',
functionAppName: 'Functions',
customCodeFunctionName: 'ProcessOrder',
functionAppNamespace: 'Contoso.Functions',
targetFramework: TargetFramework.Net8,
...overrides,
} as any;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export class FunctionFileStep extends AzureWizardPromptStep<IProjectWizardContex
private csTemplateFileName = {
[TargetFramework.NetFx]: 'FunctionsFileNetFx',
[TargetFramework.Net8]: 'FunctionsFileNet8',
[TargetFramework.Net10]: 'FunctionsFileNet10',
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { getContainingWorkspace, isMultiRootWorkspace } from '../../../utils/wor
import { localize } from '../../../../localize';
import * as vscode from 'vscode';
import { getCustomCodeRuntime } from '../../../utils/debug';
import { createCsFile, createProgramFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles';
import { createCsFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles';

/**
* This class represents a prompt step that allows the user to set up an Azure Function project.
Expand All @@ -43,7 +43,6 @@ export class CreateFunctionAppFiles {

await fs.ensureDir(functionFolderPath);
await createCsFile(assetsPath, functionFolderPath, functionAppName, namespace, projectType, targetFramework);
await createProgramFile(assetsPath, functionFolderPath, namespace, projectType, targetFramework);

if (projectType === ProjectType.rulesEngine) {
await createRulesFiles(assetsPath, functionFolderPath);
Expand Down
Loading
Loading