mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-14 14:56:33 +00:00
test: run cloud sketches tests on the CI (#2092)
- fix(test): integration tests are more resilient. - run the Create integration tests with other slow tests, - queued `PUT`/`DELETE` requests to make the test timeout happy, - reduced the `/sketches/search` offset to 1/5th and - remove Create API logging. - fix(linter): ignore `lib` folder. Remove obsolete `.node_modules` pattern. - feat(ci): enable Create API integration tests. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> Co-authored-by: per1234 <accounts@perglass.com>
This commit is contained in:
parent
3f4d2745a8
commit
0b2410d49a
@ -10,7 +10,6 @@ module.exports = {
|
|||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
'node_modules/*',
|
'node_modules/*',
|
||||||
'**/node_modules/*',
|
'**/node_modules/*',
|
||||||
'.node_modules/*',
|
|
||||||
'.github/*',
|
'.github/*',
|
||||||
'.browser_modules/*',
|
'.browser_modules/*',
|
||||||
'docs/*',
|
'docs/*',
|
||||||
@ -21,6 +20,7 @@ module.exports = {
|
|||||||
'!electron-app/webpack.config.js',
|
'!electron-app/webpack.config.js',
|
||||||
'plugins/*',
|
'plugins/*',
|
||||||
'arduino-ide-extension/src/node/cli-protocol',
|
'arduino-ide-extension/src/node/cli-protocol',
|
||||||
|
'**/lib/*',
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -296,6 +296,11 @@ jobs:
|
|||||||
IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }}
|
IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }}
|
||||||
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
|
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
|
||||||
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
||||||
|
# The CREATE_* environment vars are only used to run tests. These secrets are optional. Dependent tests will
|
||||||
|
# be skipped if not available.
|
||||||
|
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
|
||||||
|
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
|
||||||
|
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
|
||||||
run: |
|
run: |
|
||||||
# See: https://www.electron.build/code-signing
|
# See: https://www.electron.build/code-signing
|
||||||
if [ $CAN_SIGN = false ]; then
|
if [ $CAN_SIGN = false ]; then
|
||||||
|
@ -179,7 +179,8 @@ export class CreateApi {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason?.status === 404) return [] as Create.Resource[];
|
if (reason?.status === 404)
|
||||||
|
return [] as Create.Resource[]; // TODO: must not swallow 404
|
||||||
else throw reason;
|
else throw reason;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -486,18 +487,12 @@ export class CreateApi {
|
|||||||
await this.run(url, init, ResponseResultProvider.NOOP);
|
await this.run(url, init, ResponseResultProvider.NOOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchCounter = 0;
|
|
||||||
private async run<T>(
|
private async run<T>(
|
||||||
requestInfo: URL,
|
requestInfo: URL,
|
||||||
init: RequestInit | undefined,
|
init: RequestInit | undefined,
|
||||||
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const fetchCount = `[${++this.fetchCounter}]`;
|
|
||||||
const fetchStart = performance.now();
|
|
||||||
const method = init?.method ? `${init.method}: ` : '';
|
|
||||||
const url = requestInfo.toString();
|
|
||||||
const response = await fetch(requestInfo.toString(), init);
|
const response = await fetch(requestInfo.toString(), init);
|
||||||
const fetchEnd = performance.now();
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let details: string | undefined = undefined;
|
let details: string | undefined = undefined;
|
||||||
try {
|
try {
|
||||||
@ -508,18 +503,7 @@ export class CreateApi {
|
|||||||
const { statusText, status } = response;
|
const { statusText, status } = response;
|
||||||
throw new CreateError(statusText, status, details);
|
throw new CreateError(statusText, status, details);
|
||||||
}
|
}
|
||||||
const parseStart = performance.now();
|
|
||||||
const result = await resultProvider(response);
|
const result = await resultProvider(response);
|
||||||
const parseEnd = performance.now();
|
|
||||||
console.debug(
|
|
||||||
`HTTP ${fetchCount} ${method}${url} [fetch: ${(
|
|
||||||
fetchEnd - fetchStart
|
|
||||||
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
|
|
||||||
2
|
|
||||||
)} ms] body: ${
|
|
||||||
typeof result === 'string' ? result : JSON.stringify(result)
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +82,13 @@ export function isNotFound(err: unknown): err is NotFoundError {
|
|||||||
return isErrorWithStatusOf(err, 404);
|
return isErrorWithStatusOf(err, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UnprocessableContentError = CreateError & { status: 422 };
|
||||||
|
export function isUnprocessableContent(
|
||||||
|
err: unknown
|
||||||
|
): err is UnprocessableContentError {
|
||||||
|
return isErrorWithStatusOf(err, 422);
|
||||||
|
}
|
||||||
|
|
||||||
function isErrorWithStatusOf(
|
function isErrorWithStatusOf(
|
||||||
err: unknown,
|
err: unknown,
|
||||||
status: number
|
status: number
|
||||||
|
@ -5,17 +5,24 @@ import {
|
|||||||
} from '@theia/core/shared/inversify';
|
} from '@theia/core/shared/inversify';
|
||||||
import { assert, expect } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import fetch from 'cross-fetch';
|
import fetch from 'cross-fetch';
|
||||||
|
import { rejects } from 'node:assert';
|
||||||
import { posix } from 'node:path';
|
import { posix } from 'node:path';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
import queryString from 'query-string';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { ArduinoPreferences } from '../../browser/arduino-preferences';
|
import { ArduinoPreferences } from '../../browser/arduino-preferences';
|
||||||
import { AuthenticationClientService } from '../../browser/auth/authentication-client-service';
|
import { AuthenticationClientService } from '../../browser/auth/authentication-client-service';
|
||||||
import { CreateApi } from '../../browser/create/create-api';
|
import { CreateApi } from '../../browser/create/create-api';
|
||||||
import { splitSketchPath } from '../../browser/create/create-paths';
|
import { splitSketchPath } from '../../browser/create/create-paths';
|
||||||
import { Create, CreateError } from '../../browser/create/typings';
|
import {
|
||||||
|
Create,
|
||||||
|
CreateError,
|
||||||
|
isNotFound,
|
||||||
|
isUnprocessableContent,
|
||||||
|
} from '../../browser/create/typings';
|
||||||
import { SketchCache } from '../../browser/widgets/cloud-sketchbook/cloud-sketch-cache';
|
import { SketchCache } from '../../browser/widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||||
import { SketchesService } from '../../common/protocol';
|
import { SketchesService } from '../../common/protocol';
|
||||||
import { AuthenticationSession } from '../../node/auth/types';
|
import { AuthenticationSession } from '../../node/auth/types';
|
||||||
import queryString from 'query-string';
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
|
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
@ -44,6 +51,11 @@ describe('create-api', () => {
|
|||||||
await cleanAllSketches();
|
await cleanAllSketches();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
this.timeout(timeout);
|
||||||
|
await cleanAllSketches();
|
||||||
|
});
|
||||||
|
|
||||||
function createContainer(accessToken: string): Container {
|
function createContainer(accessToken: string): Container {
|
||||||
const container = new Container({ defaultScope: 'Singleton' });
|
const container = new Container({ defaultScope: 'Singleton' });
|
||||||
container.load(
|
container.load(
|
||||||
@ -120,13 +132,14 @@ describe('create-api', () => {
|
|||||||
|
|
||||||
async function cleanAllSketches(): Promise<void> {
|
async function cleanAllSketches(): Promise<void> {
|
||||||
let sketches = await createApi.sketches();
|
let sketches = await createApi.sketches();
|
||||||
// Cannot delete the sketches with `await Promise.all` as all delete promise successfully resolve, but the sketch is not deleted from the server.
|
const deleteExecutionQueue = new PQueue({
|
||||||
await sketches
|
concurrency: 5,
|
||||||
.map(({ path }) => createApi.deleteSketch(path))
|
autoStart: true,
|
||||||
.reduce(async (acc, curr) => {
|
});
|
||||||
await acc;
|
sketches.forEach(({ path }) =>
|
||||||
return curr;
|
deleteExecutionQueue.add(() => createApi.deleteSketch(path))
|
||||||
}, Promise.resolve());
|
);
|
||||||
|
await deleteExecutionQueue.onIdle();
|
||||||
sketches = await createApi.sketches();
|
sketches = await createApi.sketches();
|
||||||
expect(sketches).to.be.empty;
|
expect(sketches).to.be.empty;
|
||||||
}
|
}
|
||||||
@ -229,8 +242,52 @@ describe('create-api', () => {
|
|||||||
expect(findByName(otherName, sketches)).to.be.not.undefined;
|
expect(findByName(otherName, sketches)).to.be.not.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should error with HTTP 422 when reading a file but is a directory', async () => {
|
||||||
|
const name = v4();
|
||||||
|
const content = 'void setup(){} void loop(){}';
|
||||||
|
const posixPath = toPosix(name);
|
||||||
|
|
||||||
|
await createApi.createSketch(posixPath, content);
|
||||||
|
const resources = await createApi.readDirectory(posixPath);
|
||||||
|
expect(resources).to.be.not.empty;
|
||||||
|
|
||||||
|
await rejects(createApi.readFile(posixPath), (thrown) =>
|
||||||
|
isUnprocessableContent(thrown)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error with HTTP 422 when listing a directory but is a file', async () => {
|
||||||
|
const name = v4();
|
||||||
|
const content = 'void setup(){} void loop(){}';
|
||||||
|
const posixPath = toPosix(name);
|
||||||
|
|
||||||
|
await createApi.createSketch(posixPath, content);
|
||||||
|
const mainSketchFilePath = posixPath + posixPath + '.ino';
|
||||||
|
const sketchContent = await createApi.readFile(mainSketchFilePath);
|
||||||
|
expect(sketchContent).to.be.equal(content);
|
||||||
|
|
||||||
|
await rejects(createApi.readDirectory(mainSketchFilePath), (thrown) =>
|
||||||
|
isUnprocessableContent(thrown)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error with HTTP 404 when deleting a non-existing directory via the '/files/d' endpoint", async () => {
|
||||||
|
const name = v4();
|
||||||
|
const posixPath = toPosix(name);
|
||||||
|
|
||||||
|
const sketches = await createApi.sketches();
|
||||||
|
const sketch = findByName(name, sketches);
|
||||||
|
expect(sketch).to.be.undefined;
|
||||||
|
|
||||||
|
await rejects(createApi.deleteDirectory(posixPath), (thrown) =>
|
||||||
|
isNotFound(thrown)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
['.', '-', '_'].map((char) => {
|
['.', '-', '_'].map((char) => {
|
||||||
it(`should create a new sketch with '${char}' in the sketch folder name although it's disallowed from the Create Editor`, async () => {
|
it(`should create a new sketch with '${char}' (character code: ${char.charCodeAt(
|
||||||
|
0
|
||||||
|
)}) in the sketch folder name although it's disallowed from the Create Editor`, async () => {
|
||||||
const name = `sketch${char}`;
|
const name = `sketch${char}`;
|
||||||
const posixPath = toPosix(name);
|
const posixPath = toPosix(name);
|
||||||
const newSketch = await createApi.createSketch(
|
const newSketch = await createApi.createSketch(
|
||||||
@ -300,19 +357,23 @@ describe('create-api', () => {
|
|||||||
diff < 0 ? '<' : diff > 0 ? '>' : '='
|
diff < 0 ? '<' : diff > 0 ? '>' : '='
|
||||||
} limit)`, async () => {
|
} limit)`, async () => {
|
||||||
const content = 'void setup(){} void loop(){}';
|
const content = 'void setup(){} void loop(){}';
|
||||||
const maxLimit = 50; // https://github.com/arduino/arduino-ide/pull/875
|
const maxLimit = 10;
|
||||||
const sketchCount = maxLimit + diff;
|
const sketchCount = maxLimit + diff;
|
||||||
const sketchNames = [...Array(sketchCount).keys()].map(() => v4());
|
const sketchNames = [...Array(sketchCount).keys()].map(() => v4());
|
||||||
|
|
||||||
await sketchNames
|
const createExecutionQueue = new PQueue({
|
||||||
.map((name) => createApi.createSketch(toPosix(name), content))
|
concurrency: 5,
|
||||||
.reduce(async (acc, curr) => {
|
autoStart: true,
|
||||||
await acc;
|
});
|
||||||
return curr;
|
sketchNames.forEach((name) =>
|
||||||
}, Promise.resolve() as Promise<unknown>);
|
createExecutionQueue.add(() =>
|
||||||
|
createApi.createSketch(toPosix(name), content)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await createExecutionQueue.onIdle();
|
||||||
|
|
||||||
createApi.resetRequestRecording();
|
createApi.resetRequestRecording();
|
||||||
const sketches = await createApi.sketches();
|
const sketches = await createApi.sketches(maxLimit);
|
||||||
const allRequests = createApi.requestRecording.slice();
|
const allRequests = createApi.requestRecording.slice();
|
||||||
|
|
||||||
expect(sketches.length).to.be.equal(sketchCount);
|
expect(sketches.length).to.be.equal(sketchCount);
|
Loading…
x
Reference in New Issue
Block a user