Skip to content

test: run cloud sketches tests on the CI #2092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ module.exports = {
ignorePatterns: [
'node_modules/*',
'**/node_modules/*',
'.node_modules/*',
'.github/*',
'.browser_modules/*',
'docs/*',
Expand All @@ -21,6 +20,7 @@ module.exports = {
'!electron-app/webpack.config.js',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
'**/lib/*',
],
settings: {
react: {
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ jobs:
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
run: |
# See: https://www.electron.build/code-signing
if [ $CAN_SIGN = false ]; then
Expand Down
20 changes: 2 additions & 18 deletions arduino-ide-extension/src/browser/create/create-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ export class CreateApi {
);
})
.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;
});
}
Expand Down Expand Up @@ -486,18 +487,12 @@ export class CreateApi {
await this.run(url, init, ResponseResultProvider.NOOP);
}

private fetchCounter = 0;
private async run<T>(
requestInfo: URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): 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 fetchEnd = performance.now();
if (!response.ok) {
let details: string | undefined = undefined;
try {
Expand All @@ -508,18 +503,7 @@ export class CreateApi {
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
}
const parseStart = performance.now();
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;
}

Expand Down
7 changes: 7 additions & 0 deletions arduino-ide-extension/src/browser/create/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export function isNotFound(err: unknown): err is NotFoundError {
return isErrorWithStatusOf(err, 404);
}

export type UnprocessableContentError = CreateError & { status: 422 };
export function isUnprocessableContent(
err: unknown
): err is UnprocessableContentError {
return isErrorWithStatusOf(err, 422);
}

function isErrorWithStatusOf(
err: unknown,
status: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ import {
} from '@theia/core/shared/inversify';
import { assert, expect } from 'chai';
import fetch from 'cross-fetch';
import { rejects } from 'node:assert';
import { posix } from 'node:path';
import PQueue from 'p-queue';
import queryString from 'query-string';
import { v4 } from 'uuid';
import { ArduinoPreferences } from '../../browser/arduino-preferences';
import { AuthenticationClientService } from '../../browser/auth/authentication-client-service';
import { CreateApi } from '../../browser/create/create-api';
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 { SketchesService } from '../../common/protocol';
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-assertion */
Expand Down Expand Up @@ -44,6 +51,11 @@ describe('create-api', () => {
await cleanAllSketches();
});

afterEach(async function () {
this.timeout(timeout);
await cleanAllSketches();
});

function createContainer(accessToken: string): Container {
const container = new Container({ defaultScope: 'Singleton' });
container.load(
Expand Down Expand Up @@ -120,13 +132,14 @@ describe('create-api', () => {

async function cleanAllSketches(): Promise<void> {
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.
await sketches
.map(({ path }) => createApi.deleteSketch(path))
.reduce(async (acc, curr) => {
await acc;
return curr;
}, Promise.resolve());
const deleteExecutionQueue = new PQueue({
concurrency: 5,
autoStart: true,
});
sketches.forEach(({ path }) =>
deleteExecutionQueue.add(() => createApi.deleteSketch(path))
);
await deleteExecutionQueue.onIdle();
sketches = await createApi.sketches();
expect(sketches).to.be.empty;
}
Expand Down Expand Up @@ -229,8 +242,52 @@ describe('create-api', () => {
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) => {
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 posixPath = toPosix(name);
const newSketch = await createApi.createSketch(
Expand Down Expand Up @@ -300,19 +357,23 @@ describe('create-api', () => {
diff < 0 ? '<' : diff > 0 ? '>' : '='
} limit)`, async () => {
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 sketchNames = [...Array(sketchCount).keys()].map(() => v4());

await sketchNames
.map((name) => createApi.createSketch(toPosix(name), content))
.reduce(async (acc, curr) => {
await acc;
return curr;
}, Promise.resolve() as Promise<unknown>);
const createExecutionQueue = new PQueue({
concurrency: 5,
autoStart: true,
});
sketchNames.forEach((name) =>
createExecutionQueue.add(() =>
createApi.createSketch(toPosix(name), content)
)
);
await createExecutionQueue.onIdle();

createApi.resetRequestRecording();
const sketches = await createApi.sketches();
const sketches = await createApi.sketches(maxLimit);
const allRequests = createApi.requestRecording.slice();

expect(sketches.length).to.be.equal(sketchCount);
Expand Down