From d7e9f69f139c213614ac75bdc1ca10c2a57dfe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Grzywacz?= Date: Wed, 16 Aug 2023 10:24:03 +0200 Subject: [PATCH 1/2] JST-211 Proper unit tests for work context and batch Improved function signatures for run() --- tests/mock/activity.mock.ts | 86 +++++++++++++++ tsconfig.json | 2 +- tsconfig.spec.json | 22 ++++ yajsapi/task/batch.spec.ts | 205 ++++++++++++++++++++++++++++++++++++ yajsapi/task/batch.ts | 25 ++++- yajsapi/task/work.spec.ts | 203 +++++++++++++++++++++++++++++++++++ yajsapi/task/work.ts | 58 +++++++--- 7 files changed, 583 insertions(+), 18 deletions(-) create mode 100644 tests/mock/activity.mock.ts create mode 100755 tsconfig.spec.json create mode 100644 yajsapi/task/batch.spec.ts create mode 100644 yajsapi/task/work.spec.ts diff --git a/tests/mock/activity.mock.ts b/tests/mock/activity.mock.ts new file mode 100644 index 000000000..b9d36b784 --- /dev/null +++ b/tests/mock/activity.mock.ts @@ -0,0 +1,86 @@ +import { Activity, ActivityConfig, ActivityStateEnum, Result, ResultState } from "../../yajsapi/activity"; +import { Events, nullLogger } from "../../yajsapi"; +import { ExeScriptRequest } from "../../yajsapi/activity/activity"; +import { Readable } from "stream"; + +export class ActivityMock extends Activity { + private _currentState: ActivityStateEnum = ActivityStateEnum.Ready; + + private results: (Result | Error)[] = []; + + static createResult(props?: Partial): Result { + return { + result: ResultState.OK, + index: 1, + eventDate: new Date().toISOString(), + ...props, + }; + } + + constructor(id?: string, agreementId?: string, options?: ActivityConfig) { + super(id, agreementId, (options ?? { logger: nullLogger() }) as unknown as ActivityConfig); + } + + async execute(script: ExeScriptRequest, stream?: boolean, timeout?: number): Promise { + // TODO: add more events if needed. + const eventTarget = this.options?.eventTarget; + + const readable = new Readable({ + objectMode: true, + read: () => { + const result = this.results.shift(); + if (result instanceof Error) { + readable.emit("error", result); + } else if (result) { + readable.push(result); + } else { + eventTarget?.dispatchEvent( + new Events.ScriptExecuted({ + activityId: this.id, + agreementId: this.agreementId, + success: true, + }), + ); + readable.push(null); + } + }, + }); + + return readable; + } + + async stop(): Promise { + return true; + } + + async getState(): Promise { + return this._currentState; + } + + mockCurrentState(state: ActivityStateEnum) { + this._currentState = state; + } + + mockResults(results: Result[]) { + this.results = results; + } + + /** + * Create a new execution result and add it to the list of results. + * @param props + */ + mockResultCreate(props: Partial = {}): Result { + const result = ActivityMock.createResult(props); + this.results.push(result); + return result; + } + + /** + * Create a failure event, once execute will reach it, an exception will be thrown. + * + * This can be used to simulate various failures modes. + */ + mockResultFailure(messageOrError: string | Error): void { + this.results.push(typeof messageOrError === "string" ? new Error(messageOrError) : messageOrError); + } +} diff --git a/tsconfig.json b/tsconfig.json index b147e71c4..0c16bf96c 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,5 +29,5 @@ "excludeExternals": true, "excludeInternal": true }, - "exclude": ["dist", "node_modules", "examples", "tests", "cypress.config.ts"] + "exclude": ["dist", "node_modules", "examples", "tests", "cypress.config.ts", "**/*.spec.ts"] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100755 index 000000000..3c297e895 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "esnext", + "noImplicitAny": false, + "target": "es2018", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "removeComments": false, + "sourceMap": true, + "noLib": false, + "declaration": true, + "useUnknownInCatchVariables": false, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "lib": ["es2015", "es2016", "es2017", "es2018", "esnext", "dom"], + "outDir": "dist", + "typeRoots": ["node_modules/@types"] + }, + "exclude": ["dist", "node_modules", "examples", "cypress.config.ts"], + "include": ["yajsapi/**/*.spec.ts", "tests/unit/**/*.test.ts", "tests/unit/**/*.spec.ts"] +} diff --git a/yajsapi/task/batch.spec.ts b/yajsapi/task/batch.spec.ts new file mode 100644 index 000000000..188f9614c --- /dev/null +++ b/yajsapi/task/batch.spec.ts @@ -0,0 +1,205 @@ +import { DownloadFile, Run, UploadData, UploadFile } from "../script"; +import { Batch } from "./batch"; +import { NullStorageProvider } from "../storage"; +import { ActivityMock } from "../../tests/mock/activity.mock"; +import { LoggerMock } from "../../tests/mock"; +import { Result } from "../activity"; + +describe("Batch", () => { + let activity: ActivityMock; + let batch: Batch; + + beforeEach(() => { + activity = new ActivityMock(); + batch = new Batch(activity, new NullStorageProvider(), new LoggerMock()); + }); + + describe("Commands", () => { + describe("run()", () => { + it("should accept shell command", async () => { + expect(batch.run("rm -rf")).toBe(batch); + expect(batch["script"]["commands"][0]).toBeInstanceOf(Run); + // TODO: check if constructed script is correct. + }); + + it("should accept executable", async () => { + expect(batch.run("/bin/bash", ["-c", "echo Hello"])).toBe(batch); + expect(batch["script"]["commands"][0]).toBeInstanceOf(Run); + }); + }); + + describe("uploadFile()", () => { + it("should add upload file command", async () => { + expect(batch.uploadFile("/tmp/file.txt", "/golem/file.txt")).toBe(batch); + const cmd = batch["script"]["commands"][0] as UploadFile; + expect(cmd).toBeInstanceOf(UploadFile); + expect(cmd["src"]).toBe("/tmp/file.txt"); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + }); + }); + + describe("uploadJson()", () => { + it("should execute upload json command", async () => { + const input = { hello: "world" }; + expect(batch.uploadJson(input, "/golem/file.txt")).toBe(batch); + const cmd = batch["script"]["commands"][0] as UploadData; + expect(cmd).toBeInstanceOf(UploadData); + expect(JSON.parse(new TextDecoder().decode(cmd["src"]))).toEqual(input); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + }); + }); + + describe("uploadData()", () => { + it("should execute upload json command", async () => { + const input = "Hello World"; + expect(batch.uploadData(new TextEncoder().encode(input), "/golem/file.txt")).toBe(batch); + const cmd = batch["script"]["commands"][0] as UploadData; + expect(cmd).toBeInstanceOf(UploadData); + expect(new TextDecoder().decode(cmd["src"])).toEqual(input); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + }); + }); + + describe("downloadFile()", () => { + it("should execute download file command", async () => { + expect(batch.downloadFile("/golem/file.txt", "/tmp/file.txt")).toBe(batch); + const cmd = batch["script"]["commands"][0] as DownloadFile; + expect(cmd).toBeInstanceOf(DownloadFile); + expect(cmd["srcPath"]).toBe("/golem/file.txt"); + expect(cmd["dstPath"]).toBe("/tmp/file.txt"); + }); + }); + }); + + describe("end()", () => { + beforeEach(() => { + batch.run("echo 'Hello World'").run("echo 'Hello World 2'"); + }); + + it("should work", async () => { + activity.mockResultCreate({ stdout: "Hello World" }); + activity.mockResultCreate({ stdout: "Hello World 2" }); + + const results = await batch.end(); + + expect(results.length).toBe(2); + expect(results[0].stdout).toBe("Hello World"); + expect(results[1].stdout).toBe("Hello World 2"); + }); + + it("should initialize script with script.before()", async () => { + const spy = jest.spyOn(batch["script"], "before"); + + await batch.end(); + + expect(spy).toHaveBeenCalled(); + }); + + it("should call script.after() on success", async () => { + const spy = jest.spyOn(batch["script"], "after"); + + await batch.end(); + + expect(spy).toHaveBeenCalled(); + }); + + it("should call script.after() on failure", async () => { + const spy = jest.spyOn(batch["script"], "after"); + activity.mockResultFailure("FAILURE"); + + await expect(batch.end()).rejects.toThrowError(); + + expect(spy).toHaveBeenCalled(); + }); + + // FIXME: Not working due to bug: JST-250 + xit("should call script.after() on execute error", async () => { + const spy = jest.spyOn(batch["script"], "after"); + jest.spyOn(activity, "execute").mockRejectedValue(new Error("ERROR")); + + await expect(batch.end()).rejects.toThrowError("ERROR"); + + expect(spy).toHaveBeenCalled(); + }); + + it("should throw error on result stream error", async () => { + activity.mockResultFailure("FAILURE"); + await expect(batch.end()).rejects.toThrowError("FAILURE"); + }); + }); + + describe("endStream()", () => { + beforeEach(() => { + batch.run("echo 'Hello World'").run("echo 'Hello World 2'"); + }); + + it("should work", async () => { + activity.mockResultCreate({ stdout: "Hello World" }); + activity.mockResultCreate({ stdout: "Hello World 2" }); + const results: Result[] = []; + + const stream = await batch.endStream(); + + for await (const result of stream) { + results.push(result); + } + + expect(results.length).toBe(2); + expect(results[0].stdout).toBe("Hello World"); + expect(results[1].stdout).toBe("Hello World 2"); + }); + + it("should initialize script with script.before()", async () => { + const spy = jest.spyOn(batch["script"], "before"); + + await batch.endStream(); + + expect(spy).toHaveBeenCalled(); + }); + + it("should call script.after() on success", async () => { + const spy = jest.spyOn(batch["script"], "after"); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const r of await batch.endStream()) { + /* empty */ + } + + expect(spy).toHaveBeenCalled(); + }); + + // FIXME: Not working due to bug: JST-252 + xit("should call script.after() on result stream error", async () => { + const spy = jest.spyOn(batch["script"], "after"); + activity.mockResultFailure("FAILURE"); + + const stream = await batch.endStream(); + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const r of stream) { + /* empty */ + } + fail("Expected to throw"); + } catch (e) { + /* empty */ + } + + expect(spy).toHaveBeenCalled(); + }); + + // FIXME: Not working due to bug: JST-250 + xit("should call script.after() on execute error", async () => { + const spy = jest.spyOn(batch["script"], "after"); + jest.spyOn(activity, "execute").mockRejectedValue(new Error("ERROR")); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const r of await batch.endStream()) { + /* empty */ + } + }).rejects.toThrowError("ERROR"); + + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/yajsapi/task/batch.ts b/yajsapi/task/batch.ts index ddf1f9a78..ef0e2d449 100644 --- a/yajsapi/task/batch.ts +++ b/yajsapi/task/batch.ts @@ -20,10 +20,27 @@ export class Batch { this.script = new Script([]); } - run(...args: Array): Batch { - this.script.add( - args.length === 1 ? new Run("/bin/sh", ["-c", args[0]]) : new Run(args[0], args[1]), - ); + /** + * Execute a command on provider using a shell (/bin/sh). + * + * @param commandLine Shell command to execute. + */ + run(commandLine: string): Batch; + + /** + * Execute an executable on provider. + * + * @param executable Executable to run. + * @param args Executable arguments. + */ + run(executable: string, args: string[]): Batch; + + run(executableOrCommand: string, executableArgs?: string[]): Batch { + if (executableArgs) { + this.script.add(new Run(executableOrCommand, executableArgs)); + } else { + this.script.add(new Run("/bin/sh", ["-c", executableOrCommand])); + } return this; } diff --git a/yajsapi/task/work.spec.ts b/yajsapi/task/work.spec.ts new file mode 100644 index 000000000..5f65972e9 --- /dev/null +++ b/yajsapi/task/work.spec.ts @@ -0,0 +1,203 @@ +import { Batch, WorkContext } from "./index"; +import { LoggerMock } from "../../tests/mock"; +import { ActivityStateEnum, ResultState } from "../activity"; +import { DownloadData, DownloadFile, Run, Script, UploadData, UploadFile } from "../script"; +import { ActivityMock } from "../../tests/mock/activity.mock"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const logger = new LoggerMock(); +describe("Work Context", () => { + let context: WorkContext; + let activity: ActivityMock; + + beforeEach(() => { + logger.clear(); + activity = new ActivityMock(); + context = new WorkContext(activity, { + logger: logger, + isRunning: jest.fn(), + }); + }); + + describe("Commands", () => { + let runSpy: jest.SpyInstance; + + beforeEach(() => { + runSpy = jest.spyOn(context as any, "runOneCommand"); + }); + + describe("run()", () => { + it("should execute run command", async () => { + const result = ActivityMock.createResult({ stdout: "Ok" }); + runSpy.mockImplementation((cmd) => { + expect(cmd).toBeInstanceOf(Run); + return Promise.resolve(result); + }); + expect(await context.run("rm -rf")).toBe(result); + }); + + it("should execute run command", async () => { + const result = ActivityMock.createResult({ stdout: "Ok" }); + runSpy.mockImplementation((cmd) => { + expect(cmd).toBeInstanceOf(Run); + return Promise.resolve(result); + }); + expect(await context.run("/bin/ls", ["-R"])).toBe(result); + }); + }); + + describe("uploadFile()", () => { + it("should execute upload file command", async () => { + const result = ActivityMock.createResult(); + runSpy.mockImplementation((cmd: UploadFile) => { + expect(cmd).toBeInstanceOf(UploadFile); + expect(cmd["src"]).toBe("/tmp/file.txt"); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + + return Promise.resolve(result); + }); + expect(await context.uploadFile("/tmp/file.txt", "/golem/file.txt")).toBe(result); + }); + }); + + describe("uploadJson()", () => { + it("should execute upload json command", async () => { + const input = { hello: "world" }; + const result = ActivityMock.createResult(); + runSpy.mockImplementation((cmd: UploadData) => { + expect(cmd).toBeInstanceOf(UploadData); + const data = new TextDecoder().decode(cmd["src"]); + expect(JSON.parse(data)).toEqual(input); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + + return Promise.resolve(result); + }); + expect(await context.uploadJson(input, "/golem/file.txt")).toBe(result); + }); + }); + + describe("uploadData()", () => { + it("should execute upload json command", async () => { + const input = "Hello World"; + const result = ActivityMock.createResult(); + runSpy.mockImplementation((cmd: UploadData) => { + expect(cmd).toBeInstanceOf(UploadData); + expect(new TextDecoder().decode(cmd["src"])).toEqual(input); + expect(cmd["dstPath"]).toBe("/golem/file.txt"); + + return Promise.resolve(result); + }); + expect(await context.uploadData(new TextEncoder().encode(input), "/golem/file.txt")).toBe(result); + }); + }); + + describe("downloadFile()", () => { + it("should execute download file command", async () => { + const result = ActivityMock.createResult(); + runSpy.mockImplementation((cmd: UploadData) => { + expect(cmd).toBeInstanceOf(DownloadFile); + expect(cmd["srcPath"]).toBe("/golem/file.txt"); + expect(cmd["dstPath"]).toBe("/tmp/file.txt"); + + return Promise.resolve(result); + }); + expect(await context.downloadFile("/golem/file.txt", "/tmp/file.txt")).toBe(result); + }); + }); + + describe("downloadJson()", () => { + it("should execute download json command", async () => { + const json = { hello: "World" }; + const data = new TextEncoder().encode(JSON.stringify(json)).buffer; + const resultInput = ActivityMock.createResult({ data: data }); + runSpy.mockImplementation((cmd: DownloadData) => { + expect(cmd).toBeInstanceOf(DownloadData); + expect(cmd["srcPath"]).toBe("/golem/file.txt"); + + return Promise.resolve(resultInput); + }); + + const result = await context.downloadJson("/golem/file.txt"); + expect(result.result).toEqual(ResultState.OK); + expect(result.data).toEqual(json); + }); + }); + + describe("downloadData()", () => { + it("should execute download data command", async () => { + const result = ActivityMock.createResult({ data: new Uint8Array(10) }); + runSpy.mockImplementation((cmd: UploadData) => { + expect(cmd).toBeInstanceOf(DownloadData); + expect(cmd["srcPath"]).toBe("/golem/file.txt"); + + return Promise.resolve(result); + }); + expect(await context.downloadData("/golem/file.txt")).toBe(result); + }); + }); + + describe("runOneCommand()", () => { + it("should abort if script.before() fails", async () => { + jest.spyOn(Script.prototype, "before").mockRejectedValue(new Error("[test]")); + try { + await context["runOneCommand"](new Run("test")); + fail("Should throw error"); + } catch (e) { + expect(e.message).toContain("[test]"); + } + }); + + it("should return result on success", async () => { + jest.spyOn(Script.prototype, "before").mockResolvedValue(undefined); + activity.mockResults([ActivityMock.createResult({ stdout: "SUCCESS" })]); + const result = await context["runOneCommand"](new Run("test")); + expect(result.result).toEqual(ResultState.OK); + expect(result.stdout).toEqual("SUCCESS"); + }); + + it("should handle error result", async () => { + jest.spyOn(Script.prototype, "before").mockResolvedValue(undefined); + activity.mockResults([ActivityMock.createResult({ result: ResultState.ERROR, stdout: "FAILURE" })]); + const result = await context["runOneCommand"](new Run("test")); + expect(result.result).toEqual(ResultState.ERROR); + expect(result.stdout).toEqual("FAILURE"); + await logger.expectToInclude("Task error on provider"); + }); + }); + }); + + describe("getState()", () => { + it("should return activity state", async () => { + activity.mockCurrentState(ActivityStateEnum.Deployed); + await expect(context.getState()).resolves.toEqual(ActivityStateEnum.Deployed); + activity.mockCurrentState(ActivityStateEnum.Ready); + await expect(context.getState()).resolves.toEqual(ActivityStateEnum.Ready); + }); + }); + + describe("getWebsocketUri()", () => { + it("should throw error if there is no network node", () => { + expect(context["networkNode"]).toBeUndefined(); + }); + + it("should return websocket URI", () => { + (context as any)["networkNode"] = { + getWebsocketUri: (port: number) => `ws://localhost:${port}`, + }; + const spy = jest.spyOn(context["networkNode"] as any, "getWebsocketUri").mockReturnValue("ws://local"); + expect(context.getWebsocketUri(20)).toEqual("ws://local"); + expect(spy).toHaveBeenCalledWith(20); + }); + }); + + describe("beginBatch()", () => { + it("should create a batch object", () => { + const o = {}; + const spy = jest.spyOn(Batch, "create").mockReturnValue(o as any); + const result = context.beginBatch(); + expect(result).toBe(o); + expect(spy).toHaveBeenCalledWith(context["activity"], context["storageProvider"], context["logger"]); + }); + }); +}); diff --git a/yajsapi/task/work.ts b/yajsapi/task/work.ts index 32efc0e09..3ecff0586 100644 --- a/yajsapi/task/work.ts +++ b/yajsapi/task/work.ts @@ -1,4 +1,4 @@ -import { Activity, Result } from "../activity"; +import { Activity, ActivityStateEnum, Result, ResultState } from "../activity"; import { Capture, Command, @@ -12,8 +12,7 @@ import { UploadFile, } from "../script"; import { NullStorageProvider, StorageProvider } from "../storage"; -import { ActivityStateEnum } from "../activity"; -import { sleep, Logger } from "../utils"; +import { Logger, sleep } from "../utils"; import { Batch } from "./batch"; import { NetworkNode } from "../network"; @@ -108,16 +107,34 @@ export class WorkContext { } if (this.options?.initWorker) await this.options?.initWorker(this, undefined); } - async run(...args: Array): Promise { - const options: CommandOptions | undefined = - typeof args?.[1] === "object" ? args?.[1] : args?.[2]; - const command = - args.length === 1 - ? new Run("/bin/sh", ["-c", args[0]], options?.env, options?.capture) - : new Run(args[0], args[1], options?.env, options?.capture); - - return this.runOneCommand(command, options); + + /** + * Execute a command on provider using a shell (/bin/sh). + * + * @param commandLine Shell command to execute. + * @param options Additional run options. + */ + async run(commandLine: string, options?: CommandOptions): Promise; + + /** + * Execute an executable on provider. + * + * @param executable Executable to run. + * @param args Executable arguments. + * @param options Additional run options. + */ + async run(executable: string, args: string[], options?: CommandOptions): Promise; + async run(exeOrCmd: string, argsOrOptions?: string[] | CommandOptions, options?: CommandOptions): Promise { + const isArray = Array.isArray(argsOrOptions); + + const run = isArray + ? new Run(exeOrCmd, argsOrOptions as string[], options?.env, options?.capture) + : new Run("/bin/sh", ["-c", exeOrCmd], argsOrOptions?.env, argsOrOptions?.capture); + const runOptions = isArray ? options : (argsOrOptions as CommandOptions); + + return this.runOneCommand(run, runOptions); } + async uploadFile(src: string, dst: string, options?: CommandOptions): Promise { return this.runOneCommand(new UploadFile(this.storageProvider, src, dst), options); } @@ -127,9 +144,11 @@ export class WorkContext { const src = new TextEncoder().encode(JSON.stringify(json)); return this.runOneCommand(new UploadData(this.storageProvider, src, dst), options); } + uploadData(data: Uint8Array, dst: string, options?: CommandOptions): Promise { return this.runOneCommand(new UploadData(this.storageProvider, data, dst), options); } + downloadFile(src: string, dst: string, options?: CommandOptions): Promise { return this.runOneCommand(new DownloadFile(this.storageProvider, src, dst), options); } @@ -141,7 +160,7 @@ export class WorkContext { // eslint-disable-next-line @typescript-eslint/no-explicit-any async downloadJson(src: string, options?: CommandOptions): Promise> { const result = await this.downloadData(src, options); - if (result.result !== "Ok") { + if (result.result !== ResultState.OK) { return { ...result, data: undefined, @@ -157,9 +176,14 @@ export class WorkContext { beginBatch() { return Batch.create(this.activity, this.storageProvider, this.logger); } + + /** + * @Deprecated This function is only used to throw errors from unit tests. It should be removed. + */ rejectResult(msg: string) { throw new Error(`Work rejected. Reason: ${msg}`); } + getWebsocketUri(port: number): string { if (!this.networkNode) throw new Error("There is no network in this work context"); return this.networkNode?.getWebsocketUri(port); @@ -170,6 +194,7 @@ export class WorkContext { } private async runOneCommand(command: Command, options?: CommandOptions): Promise> { + // Initialize script. const script = new Script([command]); await script.before().catch((e) => { throw new Error( @@ -179,6 +204,8 @@ export class WorkContext { ); }); await sleep(100, true); + + // Send script. const results = await this.activity.execute(script.getExeScriptRequest(), false, options?.timeout).catch((e) => { throw new Error( `Script execution failed for command: ${JSON.stringify(command.toJson())}. ${ @@ -186,9 +213,13 @@ export class WorkContext { }`, ); }); + + // Process result. let allResults: Result[] = []; for await (const result of results) allResults.push(result); allResults = (await script.after(allResults)) as Result[]; + + // Handle errors. const commandsErrors = allResults.filter((res) => res.result === "Error"); if (commandsErrors.length) { const errorMessage = commandsErrors @@ -196,6 +227,7 @@ export class WorkContext { .join(". "); this.logger?.warn(`Task error on provider ${this.provider?.name || "'unknown'"}. ${errorMessage}`); } + return allResults[0]; } } From 29d870a04e32425b1e1b16fef413e92b73e9865c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Grzywacz?= Date: Wed, 16 Aug 2023 14:07:55 +0200 Subject: [PATCH 2/2] build: excluded **/*.spec.ts files from default tsconfig --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index f318f3e72..3cfa5b16a 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,5 +29,6 @@ "excludeExternals": true, "excludeInternal": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["**/*.spec.ts"] }