Skip to content

Commit 7bdeaa2

Browse files
authored
Merge branch 'main' into ow_merge
2 parents d873eac + 0cad6d1 commit 7bdeaa2

15 files changed

Lines changed: 334 additions & 120 deletions

.github/workflows/nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ jobs:
271271

272272
- name: Publish Test Results
273273
if: always() && steps.tests.conclusion != 'skipped'
274-
uses: dorny/test-reporter@3d76b34a4535afbd0600d347b09a6ee5deb3ed7f # v2.6.0
274+
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
275275
with:
276276
name: Playwright Tests
277277
path: e2e-report/results.xml

.vscodeignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ jestSetupFile.js
3333
.env
3434
update_copyright_years.*
3535
api/**
36+
e2e-report/**
37+
e2e-screenshots/**
38+
test-results/**
39+
.qlty/**

src/solutions/language-features/reference-link-provider.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { URI as Uri, Utils as UriUtils } from 'vscode-uri';
2121
import * as path from 'path';
2222
import { solutionManagerFactory } from '../solution-manager.factories';
2323
import { SolutionRpcDataMock } from '../solution-rpc-data.factory';
24+
import * as pathUtils from '../../utils/path-utils';
2425

2526
describe('ReferenceLinkProvider', () => {
2627
it('returns no links if the document cannot be parsed', () => {
@@ -63,7 +64,7 @@ describe('ReferenceLinkProvider', () => {
6364
const rpcData = solutionManager.getRpcData() as SolutionRpcDataMock;
6465
rpcData.seedVariables('my.Build+Target', { 'Board-layer': 'mylayer.clayer.yml' });
6566

66-
const provider = new ReferenceLinkProvider(solutionManager);
67+
const provider = new ReferenceLinkProvider(solutionManager, false);
6768
const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() });
6869

6970
expect(output).toEqual([
@@ -72,4 +73,51 @@ describe('ReferenceLinkProvider', () => {
7273
expect.objectContaining({ target: expect.objectContaining({ fsPath: UriUtils.joinPath(documentUri, '../mylayer.clayer.yml').fsPath }) }),
7374
]);
7475
});
76+
77+
it('returns links for cbuild files without expanding RPC variables', () => {
78+
const getCmsisPackRootSpy = jest.spyOn(pathUtils, 'getCmsisPackRoot').mockReturnValue('TEST_CMSIS_PACK_ROOT');
79+
80+
try {
81+
const cbuildDoc = `
82+
build-idx:
83+
csolution: ./my.csolution.yml
84+
cbuilds:
85+
- cbuild: ./my/debug/my.cbuild.yml
86+
clayers:
87+
- clayer: my.clayer.yml
88+
cprojects:
89+
- cproject: my/my.cproject.yml
90+
clayers:
91+
- clayer: my/$Board-layer$
92+
files:
93+
- file: \${CMSIS_PACK_ROOT}/myPack/0.0.1/myfile.yml
94+
`;
95+
96+
const documentFileName = path.join(__dirname, 'my.cbuild-idx.yml');
97+
const documentUri = Uri.file(documentFileName);
98+
const textDocument = textDocumentFactory({ uri: documentUri, fileName: documentFileName });
99+
textDocument.getText.mockReturnValue(cbuildDoc);
100+
101+
const solutionManager = solutionManagerFactory();
102+
const provider = new ReferenceLinkProvider(solutionManager, true);
103+
const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() });
104+
const outputPaths = output
105+
.map(link => link.target?.fsPath)
106+
.filter((target): target is string => !!target);
107+
108+
const expectedPaths = [
109+
UriUtils.joinPath(documentUri, '../my.csolution.yml').fsPath,
110+
UriUtils.joinPath(documentUri, '../my/debug/my.cbuild.yml').fsPath,
111+
UriUtils.joinPath(documentUri, '../my.clayer.yml').fsPath,
112+
UriUtils.joinPath(documentUri, '../my/my.cproject.yml').fsPath,
113+
path.join(path.sep, 'TEST_CMSIS_PACK_ROOT', 'myPack', '0.0.1', 'myfile.yml'),
114+
];
115+
116+
expectedPaths.forEach(expectedPath => {
117+
expect(outputPaths.some(outputPath => pathUtils.pathsEqual(outputPath, expectedPath))).toBe(true);
118+
});
119+
} finally {
120+
getCmsisPackRootSpy.mockRestore();
121+
}
122+
});
75123
});

src/solutions/language-features/reference-link-provider.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,25 @@ import { CancellationToken, DocumentLink, DocumentLinkProvider, Range, TextDocum
1818
import { parseYamlToCTreeItem } from '../../generic/tree-item-yaml-parser';
1919
import { CTreeItem, ETreeItemKind, ITreeItem } from '../../generic/tree-item';
2020
import type { SolutionManager } from '../solution-manager';
21+
import { getCmsisPackRoot } from '../../utils/path-utils';
2122

2223
/**
23-
* Provide links for file references in solution and project files.
24+
* Provide links for file references in solution, project, and layer files as well as for *.cbuild*.yml files.
25+
*
2426
*/
2527
export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink> {
28+
private static readonly REFERENCE_ITEM_TAGS = ['file', 'layer', 'project', 'script', 'regions',
29+
'solution', 'csolution', 'cbuild', 'clayer', 'cproject', 'cbuild-run', 'cdefault'];
30+
2631
constructor(
2732
private readonly solutionManager: SolutionManager,
33+
private readonly cbuildFile?: boolean,
2834
) {
2935
}
3036

3137
public provideDocumentLinks(textDocument: TextDocument, _token?: CancellationToken): DocumentLink[] {
3238
try {
3339
const topItem = parseYamlToCTreeItem(textDocument.getText(), textDocument.fileName);
34-
3540
return (topItem?.filterItems(item => this.isReferenceFileItem(item)) ?? [])?.flatMap((item): DocumentLink[] => {
3641
const documentLink = this.treeItemToDocumentLink(item, textDocument);
3742
return documentLink ? [documentLink] : [];
@@ -47,15 +52,30 @@ export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink>
4752
}
4853

4954
protected isReferenceFileItem(item: ITreeItem<CTreeItem>): item is CTreeItem {
55+
if (!item || item.getKind() !== ETreeItemKind.Scalar || !item.getText())
56+
return false;
5057
const tag = item.getTag();
51-
return !!tag && this.getReferenceItemTags().includes(tag);
58+
if (!tag || !this.getReferenceItemTags().includes(tag)) {
59+
return false;
60+
}
61+
if (this.cbuildFile) {
62+
// in case of cbuild-idx.yml we can only expand links under 'cbuilds' section
63+
if (tag === 'clayer' && !item.getParent('cbuilds')) {
64+
return false;
65+
}
66+
// in case of cbuild-idx.yml 'project' under 'cbuilds' is just a name, not path
67+
if (tag === 'project' && !!item.getParent('cbuilds')) {
68+
return false;
69+
}
70+
}
71+
return true;
5272
}
5373

5474
protected getReferenceItemTags(): string[] {
55-
return ['file', 'layer', 'project', 'script', 'regions'];
75+
return ReferenceLinkProvider.REFERENCE_ITEM_TAGS;
5676
}
5777

58-
private treeItemToDocumentLink(item: ITreeItem<CTreeItem> | undefined, textDocument: TextDocument) : DocumentLink | undefined {
78+
private treeItemToDocumentLink(item: ITreeItem<CTreeItem>, textDocument: TextDocument): DocumentLink | undefined {
5979
const uri = this.getUriFromItem(item);
6080
if (!uri) {
6181
return undefined;
@@ -68,20 +88,23 @@ export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink>
6888
}
6989

7090

71-
private getUriFromItem(item?: ITreeItem<CTreeItem>): Uri | undefined {
72-
if (!item || item.getKind() !== ETreeItemKind.Scalar) {
73-
return undefined;
74-
}
91+
private getUriFromItem(item: ITreeItem<CTreeItem>): Uri | undefined {
7592
let text = item.getText();
7693
if (!text) {
7794
return undefined;
7895
}
79-
const rpcData = this.solutionManager.getRpcData();
80-
const context = this.getItemContext(item);
81-
if (rpcData && context) {
82-
text = rpcData.expandString(text, context);
83-
}
8496

97+
// generated files can contain references to files in packs directory
98+
if (text.startsWith('${CMSIS_PACK_ROOT}')) {
99+
return Uri.file(text.replace('${CMSIS_PACK_ROOT}', getCmsisPackRoot()));
100+
}
101+
if (!this.cbuildFile) { // generated files have all sequences expanded
102+
const rpcData = this.solutionManager.getRpcData();
103+
const context = this.getItemContext(item);
104+
if (rpcData && context) {
105+
text = rpcData.expandString(text, context);
106+
}
107+
}
85108
const resolvedPath = item.resolvePath(text);
86109
return resolvedPath ? Uri.file(resolvedPath) : undefined;
87110
}

src/solutions/language-features/solution-language-features-provider.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@
1515
*/
1616

1717
import 'jest';
18-
import { SolutionLanguageFeaturesProvider, solutionFilesSelectors } from './solution-language-features-provider';
18+
import { SolutionLanguageFeaturesProvider, solutionBuildFilesSelectors, solutionFilesSelectors } from './solution-language-features-provider';
1919
import { ReferenceLinkProvider } from './reference-link-provider';
2020
import type { SolutionManager } from '../solution-manager';
2121

2222
describe('SolutionLanguageFeaturesProvider', () => {
23-
it('registers a document link provider for solution, project, and layer files on activation', async () => {
23+
it('registers document link providers for solution and build files on activation', async () => {
2424
const registerDocumentLinkProvider = jest.fn();
2525
const solutionManager = {} as SolutionManager;
2626
const provider = new SolutionLanguageFeaturesProvider(solutionManager, { registerDocumentLinkProvider });
2727

2828
await provider.activate({ subscriptions: [] });
2929

30-
expect(registerDocumentLinkProvider).toHaveBeenCalledWith(solutionFilesSelectors, expect.any(ReferenceLinkProvider));
31-
expect(registerDocumentLinkProvider).toHaveBeenCalledTimes(1);
30+
expect(registerDocumentLinkProvider).toHaveBeenNthCalledWith(1,
31+
solutionFilesSelectors,
32+
expect.any(ReferenceLinkProvider),
33+
);
34+
expect(registerDocumentLinkProvider).toHaveBeenNthCalledWith(2,
35+
solutionBuildFilesSelectors,
36+
expect.any(ReferenceLinkProvider),
37+
);
38+
expect(registerDocumentLinkProvider).toHaveBeenCalledTimes(2);
3239
});
3340
});

src/solutions/language-features/solution-language-features-provider.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export const solutionFilesSelectors: Readonly<DocumentSelector> = [
2626
{ pattern: '**/*.cproject.yaml' },
2727
{ pattern: '**/*.clayer.yml' },
2828
{ pattern: '**/*.clayer.yaml' },
29+
{ pattern: '**/*.cgen.yml' },
30+
{ pattern: '**/*.cgen.yaml' },
31+
];
32+
33+
export const solutionBuildFilesSelectors: Readonly<DocumentSelector> = [
34+
{ pattern: '**/*.cbuild.yml' },
35+
{ pattern: '**/*.cbuild-idx.yml' },
36+
{ pattern: '**/*.cbuild-run.yml' },
2937
];
3038

3139
export class SolutionLanguageFeaturesProvider {
@@ -36,7 +44,10 @@ export class SolutionLanguageFeaturesProvider {
3644

3745
public async activate(context: Pick<ExtensionContext, 'subscriptions'>) {
3846
context.subscriptions.push(
39-
this.languages.registerDocumentLinkProvider(solutionFilesSelectors, new ReferenceLinkProvider(this.solutionManager)),
47+
this.languages.registerDocumentLinkProvider(solutionFilesSelectors,
48+
new ReferenceLinkProvider(this.solutionManager)),
49+
this.languages.registerDocumentLinkProvider(solutionBuildFilesSelectors,
50+
new ReferenceLinkProvider(this.solutionManager, true)),
4051
);
4152
}
4253
}

src/solutions/solution-converter.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,31 @@ describe('SolutionConverter', () => {
220220
expect(completedListener).toHaveBeenCalledTimes(1);
221221
});
222222

223-
it('prints an error message to the output channel if the solution could not be converted', async () => {
223+
it('prints an error message when ListMissingPacks fails and ConvertSolution is skipped', async () => {
224224
mockCsolutionService.listMissingPacks.mockResolvedValue({ success: false });
225225
mockCsolutionService.convertSolution.mockResolvedValue({ success: false });
226226
await fireAndWaitForConversion();
227227

228228
const outputChannel = outputChannelProvider.mockGetCreatedChannelByName(manifest.CMSIS_SOLUTION_OUTPUT_CHANNEL);
229229

230+
// When listMissingPacks fails, ConvertSolution is skipped
231+
expect(outputChannel!.mockAppendedStrings).toEqual([
232+
expect.stringContaining('⚙️ Converting solution...'),
233+
expect.stringContaining('Check for missing packs...'),
234+
expect.stringContaining('Get log messages...'),
235+
expect.stringContaining('🟥 Convert solution failed'),
236+
]);
237+
238+
expect(completedListener).toHaveBeenCalledTimes(1);
239+
});
240+
241+
it('prints an error when convert solution fails', async () => {
242+
mockCsolutionService.listMissingPacks.mockResolvedValue({ success: true });
243+
mockCsolutionService.convertSolution.mockResolvedValue({ success: false });
244+
await fireAndWaitForConversion();
245+
246+
const outputChannel = outputChannelProvider.mockGetCreatedChannelByName(manifest.CMSIS_SOLUTION_OUTPUT_CHANNEL);
247+
230248
expect(outputChannel!.mockAppendedStrings).toEqual([
231249
expect.stringContaining('⚙️ Converting solution...'),
232250
expect.stringContaining('Check for missing packs...'),
@@ -303,7 +321,7 @@ describe('SolutionConverter', () => {
303321
expect(completedListener).toHaveBeenCalledTimes(1);
304322
});
305323

306-
it('get cbuild output and set diagnostics accordingly', async () => {
324+
it('get cbuild west output and set diagnostics accordingly', async () => {
307325
const mockDiagnosticsCollectionSet = jest.spyOn(vscode.languages.createDiagnosticCollection(), 'set');
308326
mockCsolutionService.convertSolution.mockResolvedValue({ success: true });
309327
mockCsolutionService.getLogMessages.mockResolvedValue({ success: true });
@@ -406,4 +424,40 @@ describe('SolutionConverter', () => {
406424
);
407425
});
408426
});
427+
428+
it('creates diagnostic when cpackget fails to download a pack', async () => {
429+
const diagnosticCollection = vscode.languages.createDiagnosticCollection();
430+
const mockDiagnosticsCollectionSet = jest.spyOn(diagnosticCollection, 'set') as unknown as jest.MockedFunction<
431+
(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[] | undefined) => void
432+
>;
433+
jest.spyOn(cmsisToolboxManager, 'runCmsisTool').mockImplementation(async (_t, _a, onOutput) => {
434+
onOutput('W: retry failed');
435+
onOutput('E: network timeout');
436+
return [1, undefined];
437+
});
438+
mockCsolutionService.listMissingPacks.mockResolvedValue({ success: true, packs: ['VendorA::PackA@1.0.0'] });
439+
mockCsolutionService.getLogMessages.mockResolvedValue({ success: true });
440+
441+
await fireAndWaitForConversion();
442+
443+
expect(mockDiagnosticsCollectionSet).toHaveBeenCalledTimes(1);
444+
const [[, diagnostics]] = mockDiagnosticsCollectionSet.mock.calls;
445+
expect(diagnostics?.[0]?.message).toContain('network timeout');
446+
expect(diagnostics?.[0]?.message).toContain('retry failed');
447+
});
448+
449+
it('extracts warnings from cbuild2cmake and csolution tool output', async () => {
450+
const mockDiagnosticsCollectionSet = jest.spyOn(vscode.languages.createDiagnosticCollection(), 'set');
451+
mockCsolutionService.convertSolution.mockResolvedValue({ success: true });
452+
mockCsolutionService.getLogMessages.mockResolvedValue({ success: true });
453+
jest.spyOn(compileCommandsGenerator, 'runCbuildSetup').mockResolvedValue([true, [
454+
'warning cbuild2cmake: some warning',
455+
'error csolution: some error',
456+
]]);
457+
458+
await fireAndWaitForConversion();
459+
460+
// Expect two calls: one for cbuild2cmake warning, one for csolution error
461+
expect(mockDiagnosticsCollectionSet).toHaveBeenCalledTimes(2);
462+
});
409463
});

0 commit comments

Comments
 (0)