testeranto
Version:
the AI powered BDD test framework for typescript projects
365 lines (328 loc) • 10.8 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-object-type */
/* eslint-disable no-async-promise-executor */
/* eslint-disable @typescript-eslint/no-explicit-any */
// Do not add logging to this file as it is used by the pure runtime.
import { PassThrough } from "stream";
import { NonEmptyObject } from "type-fest";
import type {
Ibdd_in_any,
Ibdd_out_any,
ITestImplementation,
ITestSpecification,
ITestAdapter,
} from "../CoreTypes";
import {
ITestJob,
ITLog,
IFinalResults,
ITTestResourceConfiguration,
ITTestResourceRequest,
ITestArtifactory,
defaultTestResourceRequirement,
DefaultAdapter,
} from "./index.js";
import { BaseGiven, IGivens } from "./BaseGiven";
import { BaseWhen } from "./BaseWhen.js";
import { BaseThen } from "./BaseThen.js";
import { IPM } from "./types.js";
import { BaseSuite } from "./BaseSuite";
type IExtenstions = Record<string, unknown>;
export default abstract class Tiposkripto<
I extends Ibdd_in_any = Ibdd_in_any,
O extends Ibdd_out_any = Ibdd_out_any,
M = unknown
> {
abstract receiveTestResourceConfig(
partialTestResource: string
): Promise<IFinalResults>;
specs: any;
assertThis: (t: I["then"]) => any;
testResourceRequirement: ITTestResourceRequest;
artifacts: Promise<unknown>[] = [];
testJobs: ITestJob[];
private totalTests: number;
testSpecification: ITestSpecification<I, O>;
suitesOverrides: Record<keyof IExtenstions, any>;
givenOverides: Record<keyof IExtenstions, any>;
whenOverides: Record<keyof IExtenstions, any>;
thenOverides: Record<keyof IExtenstions, any>;
puppetMaster: IPM;
constructor(
input: I["iinput"],
testSpecification: ITestSpecification<I, O>,
testImplementation: ITestImplementation<I, O, M> & {
suites: Record<string, NonEmptyObject<object>>;
givens: Record<string, any>;
whens: Record<string, any>;
thens: Record<string, any>;
},
testResourceRequirement: ITTestResourceRequest = defaultTestResourceRequirement,
testAdapter: Partial<ITestAdapter<I>> = {},
uberCatcher: (cb: () => void) => void = (cb) => cb()
) {
const fullAdapter = DefaultAdapter<I>(testAdapter);
// Create classy implementations
const classySuites = Object.entries(testImplementation.suites).reduce(
(a, [key], index) => {
a[key] = (somestring: string, givens: IGivens<I>) => {
return new (class extends BaseSuite<I, O> {
afterAll(
store: I["istore"],
artifactory: ITestArtifactory,
pm: IPM
) {
return fullAdapter.afterAll(store, pm);
}
assertThat(t: Awaited<I["then"]>): boolean {
return fullAdapter.assertThis(t);
}
async setup(
s: I["iinput"],
artifactory: ITestArtifactory,
tr: ITTestResourceConfiguration,
pm: IPM
): Promise<I["isubject"]> {
return (
fullAdapter.beforeAll?.(s, tr, pm) ??
(s as unknown as Promise<I["isubject"]>)
);
}
})(somestring, index, givens);
};
return a;
},
{}
);
const classyGivens = Object.entries(testImplementation.givens).reduce(
(a, [key, g]) => {
a[key] = (
// name: string,
features: string[],
whens: BaseWhen<I>[],
thens: BaseThen<I>[],
gcb: I["given"],
initialValues: any
) => {
// Debug the parameters being passed - check if features contains when-like objects
// console.log(`[Tiposkripto] Creating Given ${key} with:`);
// console.log(` name: ${name}`);
// console.log(` features:`, features);
// console.log(` whens:`, whens);
// console.log(` thens:`, thens);
// Ensure parameters are arrays and create copies to avoid reference issues
const safeFeatures = Array.isArray(features) ? [...features] : [];
const safeWhens = Array.isArray(whens) ? [...whens] : [];
const safeThens = Array.isArray(thens) ? [...thens] : [];
return new (class extends BaseGiven<I> {
uberCatcher = uberCatcher;
async givenThat(
subject,
testResource,
artifactory,
initializer,
initialValues,
pm
) {
return fullAdapter.beforeEach(
subject,
initializer,
testResource,
initialValues,
pm
);
}
afterEach(
store: I["istore"],
key: string,
artifactory,
pm
): Promise<unknown> {
return Promise.resolve(fullAdapter.afterEach(store, key, pm));
}
})(
// name,
safeFeatures,
safeWhens,
safeThens,
testImplementation.givens[key],
initialValues
);
};
return a;
},
{}
);
const classyWhens = Object.entries(testImplementation.whens).reduce(
(a, [key, whEn]: [string, (...x: any[]) => any]) => {
a[key] = (...payload: any[]) => {
const whenInstance = new (class extends BaseWhen<I> {
async andWhen(store, whenCB, testResource, pm) {
return await fullAdapter.andWhen(store, whenCB, testResource, pm);
}
})(`${key}: ${payload && payload.toString()}`, whEn(...payload));
// console.log(`[Tiposkripto] Created When ${key}:`, whenInstance.name);
return whenInstance;
};
return a;
},
{}
);
const classyThens = Object.entries(testImplementation.thens).reduce(
(a, [key, thEn]: [string, (...x: any[]) => any]) => {
a[key] = (...args: any[]) => {
const thenInstance = new (class extends BaseThen<I> {
async butThen(
store: any,
thenCB,
testResource: any,
pm: IPM
): Promise<I["iselection"]> {
return await fullAdapter.butThen(store, thenCB, testResource, pm);
}
})(`${key}: ${args && args.toString()}`, thEn(...args));
// console.log(`[Tiposkripto] Created Then ${key}:`, thenInstance.name);
return thenInstance;
};
return a;
},
{}
);
// Set up the overrides
this.suitesOverrides = classySuites;
this.givenOverides = classyGivens;
this.whenOverides = classyWhens;
this.thenOverides = classyThens;
this.testResourceRequirement = testResourceRequirement;
this.testSpecification = testSpecification;
// Generate specs
this.specs = testSpecification(
this.Suites(),
this.Given(),
this.When(),
this.Then()
);
this.totalTests = this.calculateTotalTests();
this.testJobs = this.specs.map((suite: BaseSuite<I, O>) => {
const suiteRunner =
(suite: BaseSuite<I, O>) =>
async (puppetMaster: IPM, tLog: ITLog): Promise<BaseSuite<I, O>> => {
try {
const x = await suite.run(
input,
puppetMaster.testResourceConfiguration,
(fPath: string, value: string | Buffer | PassThrough) =>
puppetMaster.testArtiFactoryfileWriter(
tLog,
(p: Promise<void>) => {
this.artifacts.push(p);
}
)(
puppetMaster.testResourceConfiguration.fs + "/" + fPath,
value
),
tLog,
puppetMaster
);
return x;
} catch (e) {
console.error(e.stack);
throw e;
}
};
const runner = suiteRunner(suite);
return {
test: suite,
toObj: () => {
return suite.toObj();
},
runner,
receiveTestResourceConfig: async function (
puppetMaster: IPM
): Promise<IFinalResults> {
const tLog = async (...l: string[]) => {
//
};
try {
const suiteDone: BaseSuite<I, O> = await runner(puppetMaster, tLog);
const fails = suiteDone.fails;
// Always use PM.writeFileSync to write tests.json, not direct filesystem access
await puppetMaster.writeFileSync(
`tests.json`,
JSON.stringify(this.toObj(), null, 2),
"test"
);
return {
failed: fails > 0,
fails,
artifacts: this.artifacts || [],
features: suiteDone.features(),
tests: 0, // Keep existing field
runTimeTests: this.totalTests, // Add the total number of tests
};
} catch (e) {
console.error(e.stack);
return {
failed: true,
fails: -1,
artifacts: this.artifacts || [],
features: [],
tests: 0, // Keep existing field
runTimeTests: -1, // Set to -1 on hard error
};
}
},
};
});
}
Specs() {
return this.specs;
}
Suites() {
return this.suitesOverrides;
}
Given(): Record<
keyof IExtenstions,
(
name: string,
features: string[],
whens: BaseWhen<I>[],
thens: BaseThen<I>[],
gcb: I["given"]
) => BaseGiven<I>
> {
return this.givenOverides;
}
When(): Record<
keyof IExtenstions,
(arg0: I["istore"], ...arg1: any) => BaseWhen<I>
> {
return this.whenOverides;
}
Then(): Record<
keyof IExtenstions,
(selection: I["iselection"], expectation: any) => BaseThen<I>
> {
return this.thenOverides;
}
// Add a method to access test jobs which can be used by receiveTestResourceConfig
getTestJobs(): ITestJob[] {
return this.testJobs;
}
private calculateTotalTests(): number {
let total = 0;
for (const suite of this.specs) {
if (suite && typeof suite === "object") {
// Access the givens property which should be a record of test names to BaseGiven instances
// The givens property is typically on the suite instance
if ("givens" in suite) {
const givens = (suite as any).givens;
if (givens && typeof givens === "object") {
total += Object.keys(givens).length;
}
}
}
}
return total;
}
}