Skip to content

Commit

Permalink
Http stream end to end tests (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejizba authored Feb 5, 2024
1 parent 1f17f4f commit a71ccdb
Show file tree
Hide file tree
Showing 23 changed files with 507 additions and 22 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
],
"deprecation/deprecation": "error",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"azureFunctions.showProjectWarning": false,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": ["source.fixAll"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "relative",
Expand Down
20 changes: 20 additions & 0 deletions app/v3/httpTriggerRandomDelay/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/httpTriggerRandomDelay/index.js"
}
19 changes: 19 additions & 0 deletions app/v3/httpTriggerRandomDelay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { addRandomAsyncOrSyncDelay } from '../utils/getRandomTestData';

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
context.log(`Http function processed request for url "${req.url}"`);

await addRandomAsyncOrSyncDelay();

const name = req.query.name || req.body || 'world';

context.res = {
body: `Hello, ${name}!`,
};
};

export default httpTrigger;
13 changes: 13 additions & 0 deletions app/v3/utils/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

export async function delay(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
}

export function delaySync(ms: number): void {
const endTime = Date.now() + ms;
while (Date.now() < endTime) {
// wait
}
}
32 changes: 32 additions & 0 deletions app/v3/utils/getRandomTestData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import * as crypto from 'crypto';
import { delay, delaySync } from './delay';

export function getRandomTestData(): string {
// This should start with non-numeric data to prevent this bug from causing a test failure
// https://github.com/Azure/azure-functions-nodejs-library/issues/90
return `testData${getRandomHexString()}`;
}

export function getRandomHexString(length = 10): string {
const buffer: Buffer = crypto.randomBytes(Math.ceil(length / 2));
return buffer.toString('hex').slice(0, length);
}

export function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min) + min);
}

export function getRandomBoolean(percentTrue: number): boolean {
return Math.random() * 100 > percentTrue;
}

export async function addRandomAsyncOrSyncDelay(): Promise<void> {
if (getRandomBoolean(95)) {
await delay(getRandomInt(0, 250));
} else {
delaySync(getRandomInt(0, 10));
}
}
6 changes: 6 additions & 0 deletions app/v4-oldConfig/src/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { app } from '@azure/functions';

app.setup({ enableHttpStream: false });
8 changes: 4 additions & 4 deletions app/v4/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions app/v4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"test": "echo \"No tests yet...\""
},
"dependencies": {
"@azure/functions": "^4.0.0"
"@azure/functions": "^4.2.0"
},
"devDependencies": {
"@types/long": "^4.0.0",
"@types/node": "^18.x",
"typescript": "^4.0.0",
"rimraf": "^5.0.0"
"rimraf": "^5.0.0",
"typescript": "^4.0.0"
},
"main": "dist/src/{index.js,functions/*.js}"
}
"main": "dist/src/{index.js,setup.js,functions/*.js}"
}
24 changes: 24 additions & 0 deletions app/v4/src/functions/httpTriggerRandomDelay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { addRandomAsyncOrSyncDelay } from '../utils/getRandomTestData';

export async function httpTriggerRandomDelay(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
context.log(`Http function processed request for url "${request.url}"`);

await addRandomAsyncOrSyncDelay();

const name = request.query.get('name') || (await request.text()) || 'world';

return { body: `Hello, ${name}!` };
}

app.http('httpTriggerRandomDelay', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: httpTriggerRandomDelay,
});
22 changes: 22 additions & 0 deletions app/v4/src/functions/httpTriggerReceiveStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { receiveStreamWithProgress } from '../utils/streamHttp';

export async function httpTriggerReceiveStream(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
context.log(`Http function processed request for url "${request.url}"`);

const bytesReceived = await receiveStreamWithProgress(request.body);

return { body: `Bytes received: ${bytesReceived}` };
}

app.http('httpTriggerReceiveStream', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: httpTriggerReceiveStream,
});
22 changes: 22 additions & 0 deletions app/v4/src/functions/httpTriggerSendStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { createRandomStream } from '../utils/streamHttp';

export async function httpTriggerSendStream(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
context.log(`Http function processed request for url "${request.url}"`);

const lengthInMb = request.query.get('lengthInMb');
const stream = createRandomStream(Number(lengthInMb));
return { body: stream };
}

app.http('httpTriggerSendStream', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: httpTriggerSendStream,
});
6 changes: 6 additions & 0 deletions app/v4/src/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { app } from '@azure/functions';

app.setup({ enableHttpStream: true });
13 changes: 13 additions & 0 deletions app/v4/src/utils/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

export async function delay(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
}

export function delaySync(ms: number): void {
const endTime = Date.now() + ms;
while (Date.now() < endTime) {
// wait
}
}
32 changes: 32 additions & 0 deletions app/v4/src/utils/getRandomTestData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import * as crypto from 'crypto';
import { delay, delaySync } from './delay';

export function getRandomTestData(): string {
// This should start with non-numeric data to prevent this bug from causing a test failure
// https://github.com/Azure/azure-functions-nodejs-library/issues/90
return `testData${getRandomHexString()}`;
}

export function getRandomHexString(length = 10): string {
const buffer: Buffer = crypto.randomBytes(Math.ceil(length / 2));
return buffer.toString('hex').slice(0, length);
}

export function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min) + min);
}

export function getRandomBoolean(percentTrue: number): boolean {
return Math.random() * 100 > percentTrue;
}

export async function addRandomAsyncOrSyncDelay(): Promise<void> {
if (getRandomBoolean(95)) {
await delay(getRandomInt(0, 250));
} else {
delaySync(getRandomInt(0, 10));
}
}
57 changes: 57 additions & 0 deletions app/v4/src/utils/streamHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import * as crypto from 'crypto';
import { Readable } from 'stream';
import { delay } from './delay';

const oneMb = 1024 * 1024;

export function createRandomStream(lengthInMb: number): Readable {
const stream = new Readable();
stream._read = () => {};
setTimeout(() => {
void sendRandomData(stream, lengthInMb);
}, 5);
return stream;
}

async function sendRandomData(stream: Readable, lengthInMb: number): Promise<void> {
const maxChunkSize = oneMb;
let remainingBytes = convertMbToB(lengthInMb);
do {
if (stream.readableLength > maxChunkSize) {
await delay(5);
} else {
const chunkSize = Math.min(maxChunkSize, remainingBytes);
stream.push(crypto.randomBytes(chunkSize));
remainingBytes -= chunkSize;
}
} while (remainingBytes > 0);
stream.push(null);
}

export async function receiveStreamWithProgress(stream: {
[Symbol.asyncIterator](): AsyncIterableIterator<string | Buffer>;
}): Promise<number> {
let bytesReceived = 0;
const logInterval = 500;
let nextLogTime = Date.now();
for await (const chunk of stream) {
if (Date.now() > nextLogTime) {
nextLogTime = Date.now() + logInterval;
console.log(`Progress: ${convertBToMb(bytesReceived)}mb`);
}

bytesReceived += chunk.length;
}
return bytesReceived;
}

export function convertMbToB(mb: number): number {
return mb * oneMb;
}

function convertBToMb(bytes: number) {
return Math.round(bytes / oneMb);
}
28 changes: 24 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a71ccdb

Please sign in to comment.