Skip to content

Commit

Permalink
chore: add integration test WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
eunjae-lee committed Jan 13, 2022
1 parent 7eb9fcf commit 9da2716
Show file tree
Hide file tree
Showing 12 changed files with 586 additions and 79 deletions.
87 changes: 87 additions & 0 deletions tests/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import openapitools from '../openapitools.json';
import fsp from 'fs/promises';
import path from 'path';

export const availableLanguages = ['javascript'] as const;
export type Language = typeof availableLanguages[number];

function printUsage(commandName: string): void {
console.log(`usage: ${commandName} all | language1 language2...`);
console.log(`\tavailable languages: ${availableLanguages.join(',')}`);
// eslint-disable-next-line no-process-exit
process.exit(1);
}

export async function* walk(
dir: string
): AsyncGenerator<{ path: string; name: string }> {
for await (const d of await fsp.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) yield* walk(entry);
else if (d.isFile()) yield { path: entry, name: d.name };
}
}

export const extensionForLanguage: Record<Language, string> = {
javascript: '.test.ts',
};

// For each generator, we map the packageName with the language and client
export const packageNames: Record<
string,
Record<Language, string>
> = Object.entries(openapitools['generator-cli'].generators).reduce(
(prev, [clientName, clientConfig]) => {
const obj = prev;
const parts = clientName.split('-');
const lang = parts[0] as Language;
const client = parts.slice(1).join('-');

if (!(lang in prev)) {
obj[lang] = {};
}

obj[lang][client] = clientConfig.additionalProperties.packageName;

return obj;
},
{} as Record<string, Record<string, string>>
);

function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

export function createClientName(client: string): string {
return `${client
.split('-')
.map((part) => capitalize(part))
.join('')}Api`;
}

export async function parseCLI(
args: string[],
commandName: string
): Promise<Language[]> {
if (args.length < 3) {
console.log('not enough arguments');
printUsage(commandName);
}

let toGenerate: Language[];
if (args.length === 3 && args[2] === 'all') {
toGenerate = [...availableLanguages];
} else {
const languages = args[2].split(' ') as Language[];
const unknownLanguages = languages.filter(
(lang) => !availableLanguages.includes(lang)
);
if (unknownLanguages.length > 0) {
console.log('unkown language(s): ', unknownLanguages.join(', '));
printUsage(commandName);
}
toGenerate = languages;
}

return toGenerate;
}
88 changes: 10 additions & 78 deletions tests/generateCTS.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/* eslint-disable no-console */
import fsp from 'fs/promises';
import path from 'path';

import SwaggerParser from '@apidevtools/swagger-parser';
import Mustache from 'mustache';
import type { OpenAPIV3 } from 'openapi-types';

import openapitools from '../openapitools.json';
import {
walk,
parseCLI,
packageNames,
createClientName,
extensionForLanguage,
type Language,
} from './common';

const availableLanguages = ['javascript'] as const;
type Language = typeof availableLanguages[number];
type ParametersWithDataType = {
key: string;
value: string;
Expand Down Expand Up @@ -43,55 +47,12 @@ type CTSBlock = {
// Array of test per client
type CTS = Record<string, CTSBlock[]>;

const extensionForLanguage: Record<Language, string> = {
javascript: '.test.ts',
};

const cts: CTS = {};

// For each generator, we map the packageName with the language and client
const packageNames: Record<string, Record<Language, string>> = Object.entries(
openapitools['generator-cli'].generators
).reduce((prev, [clientName, clientConfig]) => {
const obj = prev;
const parts = clientName.split('-');
const lang = parts[0] as Language;
const client = parts.slice(1).join('-');

if (!(lang in prev)) {
obj[lang] = {};
}

obj[lang][client] = clientConfig.additionalProperties.packageName;

return obj;
}, {} as Record<string, Record<string, string>>);

async function createOutputDir(language: Language): Promise<void> {
await fsp.mkdir(`output/${language}`, { recursive: true });
}

async function* walk(
dir: string
): AsyncGenerator<{ path: string; name: string }> {
for await (const d of await fsp.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) yield* walk(entry);
else if (d.isFile()) yield { path: entry, name: d.name };
}
}

function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

function createClientName(client: string): string {
return `${client
.split('-')
.map((part) => capitalize(part))
.join('')}Api`;
}

function removeObjectName(obj: Record<string, any>): void {
for (const prop in obj) {
if (prop === '$objectName') {
Expand Down Expand Up @@ -236,34 +197,7 @@ async function generateCode(language: Language): Promise<void> {
}
}

function printUsage(): void {
console.log(`usage: generateCTS all | language1 language2...`);
console.log(`\tavailable languages: ${availableLanguages.join(',')}`);
// eslint-disable-next-line no-process-exit
process.exit(1);
}

async function parseCLI(args: string[]): Promise<void> {
if (args.length < 3) {
console.log('not enough arguments');
printUsage();
}

let toGenerate: Language[];
if (args.length === 3 && args[2] === 'all') {
toGenerate = [...availableLanguages];
} else {
const languages = args[2].split(' ') as Language[];
const unknownLanguages = languages.filter(
(lang) => !availableLanguages.includes(lang)
);
if (unknownLanguages.length > 0) {
console.log('unkown language(s): ', unknownLanguages.join(', '));
printUsage();
}
toGenerate = languages;
}

parseCLI(process.argv, 'generateCTS').then(async (toGenerate) => {
try {
await loadCTS();
for (const lang of toGenerate) {
Expand All @@ -274,6 +208,4 @@ async function parseCLI(args: string[]): Promise<void> {
console.error(e);
}
}
}

parseCLI(process.argv);
});
187 changes: 187 additions & 0 deletions tests/generateIntegrationTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/* eslint-disable no-console */
import fsp from 'fs/promises';
import Mustache from 'Mustache';
import {
packageNames,
parseCLI,
walk,
createClientName,
extensionForLanguage,
type Language,
} from './common';

type TestCase = {
testName: string;
autoCreateClient?: boolean; // `true` by default
autoCreateIndex?: boolean; // `false` by default
steps: Step[];
};

type Step = CreateClientStep | VariableStep | MethodStep;

type CreateClientStep = {
type: 'createClient';
parameters: {
appId: string;
apiKey: string;
};
expected?: Expected;
};

type VariableStep = {
type: 'variable';
object: string;
path: string[];
expected?: Expected;
};

type MethodStep = {
type: 'method';
object: string;
path: string[];
parameters?: any;
expected?: Expected;
};

type Expected = {
length?: number;
error?: string;
match?: { objectContaining: object } | any;
};

type IntegrationTestsBlock = {
operationId: string;
tests: TestCase[];
};

type AllTests = {
[client: string]: IntegrationTestsBlock[];
};

async function loadIntegrationTests() {
const integrationTests: AllTests = {};
for await (const { name: client } of await fsp.opendir(
'./integration/clients/'
)) {
integrationTests[client] = await loadIntegrationTestsForClient(client);
}
return integrationTests;
}

async function loadIntegrationTestsForClient(client: string) {
const testBlocks: IntegrationTestsBlock[] = [];
for await (const file of walk(`./integration/clients/${client}`)) {
if (!file.name.endsWith('.json')) {
continue;
}
const fileName = file.name.replace('.json', '');
const fileContent = (await fsp.readFile(file.path)).toString();

if (!fileContent) {
throw new Error(`cannot read empty file ${fileName} - ${client} client`);
}

const tests: TestCase[] = JSON.parse(fileContent).map((testCase) => {
if (!testCase.testName) {
throw new Error(
`Cannot have a test with no name ${fileName} - ${client} client`
);
}
return {
autoCreateClient: true,
autoCreateIndex: false,
...testCase,
};
});

testBlocks.push({
operationId: fileName,
tests,
});
}

return testBlocks;
}

async function loadTemplates(language: Language) {
const templates: Record<string, string> = {};
for await (const file of walk(`./integration/templates/${language}`)) {
if (!file.name.endsWith('.mustache')) {
continue;
}
const type = file.name.replace('.mustache', '');
const fileContent = (await fsp.readFile(file.path)).toString();
templates[type] = fileContent;
}
return templates;
}

async function generateCode(language: Language, allClientsTests: AllTests) {
await fsp.mkdir(`output/${language}`, { recursive: true });
for (const client in allClientsTests) {
const { suite: template, ...partialTemplates } = await loadTemplates(
language
);
const code = Mustache.render(
template,
{
import: packageNames[language][client],
client: createClientName(client),
blocks: modifyForMustache(allClientsTests[client]),
},
partialTemplates
);
await fsp.writeFile(
`output/${language}/${client}.integration${extensionForLanguage[language]}`,
code
);
}
}

function modifyForMustache(blocks: IntegrationTestsBlock[]) {
return blocks.map(({ tests, ...rest }) => ({
...rest,
tests: tests.map(({ steps, ...rest }) => ({
...rest,
steps: steps.map((step) => {
const modified = {
...step,
isCreateClient: step.type === 'createClient',
isVariable: step.type === 'variable',
isMethod: step.type === 'method',
};

if (step.type === 'method') {
if (step.parameters) {
let serialized = JSON.stringify(step.parameters);
serialized = serialized.slice(1, serialized.length - 1);
// @ts-expect-error
modified.parameters = serialized;
}
}

if (step.expected && step.expected.error) {
// @ts-expect-error
modified.expectedError = step.expected.error;
}

return modified;
}),
})),
}));
}

parseCLI(process.argv, 'generateIntegrationTests').then(
async (languagesToGenerate) => {
try {
const allClientsTests = await loadIntegrationTests();
for (const language of languagesToGenerate) {
generateCode(language, allClientsTests);
}
} catch (e) {
if (e instanceof Error) {
console.error(e);
}
}
}
);
Loading

0 comments on commit 9da2716

Please sign in to comment.