k6ctl 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.github/workflows/ci.yml +61 -0
  2. package/README.md +45 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +28 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/list.d.ts +4 -0
  8. package/dist/commands/list.d.ts.map +1 -0
  9. package/dist/commands/list.js +35 -0
  10. package/dist/commands/list.js.map +1 -0
  11. package/dist/commands/run.d.ts +9 -0
  12. package/dist/commands/run.d.ts.map +1 -0
  13. package/dist/commands/run.js +76 -0
  14. package/dist/commands/run.js.map +1 -0
  15. package/dist/services/kubernetes.service.d.ts +20 -0
  16. package/dist/services/kubernetes.service.d.ts.map +1 -0
  17. package/dist/services/kubernetes.service.js +278 -0
  18. package/dist/services/kubernetes.service.js.map +1 -0
  19. package/dist/services/script.service.d.ts +8 -0
  20. package/dist/services/script.service.d.ts.map +1 -0
  21. package/dist/services/script.service.js +82 -0
  22. package/dist/services/script.service.js.map +1 -0
  23. package/dist/types/config.types.d.ts +26 -0
  24. package/dist/types/config.types.d.ts.map +1 -0
  25. package/dist/types/config.types.js +3 -0
  26. package/dist/types/config.types.js.map +1 -0
  27. package/dist/types/kubernetes.types.d.ts +9 -0
  28. package/dist/types/kubernetes.types.d.ts.map +1 -0
  29. package/dist/types/kubernetes.types.js +3 -0
  30. package/dist/types/kubernetes.types.js.map +1 -0
  31. package/dist/types/script.types.d.ts +11 -0
  32. package/dist/types/script.types.d.ts.map +1 -0
  33. package/dist/types/script.types.js +3 -0
  34. package/dist/types/script.types.js.map +1 -0
  35. package/dist/types/testRunManifest.types.d.ts +43 -0
  36. package/dist/types/testRunManifest.types.d.ts.map +1 -0
  37. package/dist/types/testRunManifest.types.js +3 -0
  38. package/dist/types/testRunManifest.types.js.map +1 -0
  39. package/dist/utils/configLoader.d.ts +30 -0
  40. package/dist/utils/configLoader.d.ts.map +1 -0
  41. package/dist/utils/configLoader.js +63 -0
  42. package/dist/utils/configLoader.js.map +1 -0
  43. package/dist/utils/env.d.ts +17 -0
  44. package/dist/utils/env.d.ts.map +1 -0
  45. package/dist/utils/env.js +111 -0
  46. package/dist/utils/env.js.map +1 -0
  47. package/dist/utils/logger.d.ts +4 -0
  48. package/dist/utils/logger.d.ts.map +1 -0
  49. package/dist/utils/logger.js +16 -0
  50. package/dist/utils/logger.js.map +1 -0
  51. package/dist/utils/table.util.d.ts +10 -0
  52. package/dist/utils/table.util.d.ts.map +1 -0
  53. package/dist/utils/table.util.js +24 -0
  54. package/dist/utils/table.util.js.map +1 -0
  55. package/dist/utils/testRunManifestBuilder.d.ts +5 -0
  56. package/dist/utils/testRunManifestBuilder.d.ts.map +1 -0
  57. package/dist/utils/testRunManifestBuilder.js +89 -0
  58. package/dist/utils/testRunManifestBuilder.js.map +1 -0
  59. package/package.json +44 -0
  60. package/src/cli.ts +31 -0
  61. package/src/commands/list.ts +29 -0
  62. package/src/commands/run.ts +52 -0
  63. package/src/services/kubernetes.service.ts +238 -0
  64. package/src/services/script.service.ts +79 -0
  65. package/src/types/config.types.ts +25 -0
  66. package/src/types/kubernetes.types.ts +9 -0
  67. package/src/types/script.types.ts +8 -0
  68. package/src/types/testRunManifest.types.ts +42 -0
  69. package/src/utils/configLoader.ts +63 -0
  70. package/src/utils/env.ts +90 -0
  71. package/src/utils/logger.ts +17 -0
  72. package/src/utils/table.util.ts +23 -0
  73. package/src/utils/testRunManifestBuilder.ts +102 -0
  74. package/test/integration/env.test.ts +33 -0
  75. package/test/integration/kubernetes.service.test.ts +70 -0
  76. package/test/integration/script.service.test.ts +64 -0
  77. package/test/jest.config.integration.js +20 -0
  78. package/test/jest.config.unit.js +14 -0
  79. package/test/samples/data_sample_1.csv +4 -0
  80. package/test/samples/k6_script_sample_1.js +9 -0
  81. package/test/samples/k6_script_sample_2.js +18 -0
  82. package/test/unit/configLoader.test.ts +53 -0
  83. package/test/unit/env.test.ts +1029 -0
  84. package/test/unit/kubernetes.service.test.ts +197 -0
  85. package/test/unit/script.service.test.ts +112 -0
  86. package/tsconfig.json +20 -0
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import logger from './logger';
4
+
5
+ const BASE_IMAGE = "grafana/k6:latest";
6
+ const DEFAULT_NAMESPACE = "default";
7
+ const DEFAULT_CONFIG_PATH = "k6ctl.config.json";
8
+ const PROMETHEUS_DEFAULT_TREND_STATS = ["avg", "p(95)", "p(99)", "min", "max"];
9
+
10
+ const ResourcesSchema = z.object({
11
+ limits: z.object({
12
+ cpu: z.string(),
13
+ memory: z.string(),
14
+ }),
15
+ requests: z.object({
16
+ cpu: z.string(),
17
+ memory: z.string(),
18
+ }),
19
+ });
20
+
21
+ const RunnerSchema = z
22
+ .object({
23
+ image: z.string(),
24
+ resources: ResourcesSchema.optional(),
25
+ })
26
+ .default({ image: BASE_IMAGE });
27
+
28
+ const PrometheusSchema = z
29
+ .object({
30
+ serverUrl: z.string(),
31
+ trendStats: z.array(z.string()).default(PROMETHEUS_DEFAULT_TREND_STATS),
32
+ });
33
+
34
+ const K6ConfigSchema = z.object({
35
+ namespace: z.string().default(DEFAULT_NAMESPACE),
36
+ parallelism: z.number().int().positive().default(1),
37
+ arguments: z
38
+ .array(z.string())
39
+ .optional(),
40
+ cleanup: z.boolean().default(false),
41
+ quiet: z.boolean().default(true),
42
+ separate: z.boolean().default(false),
43
+ runner: RunnerSchema,
44
+ prometheus: PrometheusSchema.optional(),
45
+ });
46
+
47
+ export type K6ConfigParsed = z.infer<typeof K6ConfigSchema>;
48
+
49
+ export function loadK6Config(path = DEFAULT_CONFIG_PATH): K6ConfigParsed {
50
+ if (!existsSync(path)) {
51
+ logger.warn(`Config file '${path}' doesn't exist, using default values.`);
52
+ return K6ConfigSchema.parse({});
53
+ }
54
+ const raw = readFileSync(path, "utf-8");
55
+ let obj: unknown;
56
+ try {
57
+ obj = JSON.parse(raw);
58
+ } catch (e) {
59
+ throw new Error(`Config file '${path}' exists but is not valid JSON: ${(e as Error).message}`);
60
+ }
61
+ logger.info(`Loaded config from '${path}'`);
62
+ return K6ConfigSchema.parse(obj);
63
+ }
@@ -0,0 +1,90 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { parse } from "dotenv";
4
+ import logger from './logger';
5
+
6
+ /**
7
+ * Configura tus patrones de validación aquí
8
+ */
9
+ const DEFAULT_RULES = {
10
+ // Valida nombres tipo ENV clásico: MAYUSCULAS_NUMEROS_GUIONBAJO
11
+ keyPattern: /^[A-Z][A-Z0-9_]*$/,
12
+
13
+ // Valores permitidos: ASCII imprimible (no control chars). Permite espacios.
14
+ valuePattern: /^[\x20-\x7E]*$/,
15
+ };
16
+
17
+ type Rules = typeof DEFAULT_RULES;
18
+
19
+ /**
20
+ * Lee y valida un archivo .env
21
+ * @param envFilePath - Ruta del archivo .env a cargar
22
+ * @param rules - Reglas de validación personalizadas (opcional)
23
+ * @returns Objeto con las variables de entorno validadas
24
+ */
25
+ export function loadAndValidateEnv(
26
+ envFilePath: string = ".env",
27
+ rules: Rules = DEFAULT_RULES
28
+ ): Record<string, string> {
29
+ const errors: string[] = [];
30
+ const result: Record<string, string> = {};
31
+
32
+ // Validar que el archivo existe
33
+ const absolutePath = path.isAbsolute(envFilePath)
34
+ ? envFilePath
35
+ : path.resolve(process.cwd(), envFilePath);
36
+
37
+ if (!fs.existsSync(absolutePath)) {
38
+ throw new Error(`Archivo .env no encontrado: ${absolutePath}`);
39
+ }
40
+
41
+ logger.debug(`Loading .env file from: ${absolutePath}`);
42
+
43
+ // Leer el archivo .env
44
+ let fileContent: string;
45
+ try {
46
+ fileContent = fs.readFileSync(absolutePath, "utf-8");
47
+ } catch (error) {
48
+ throw new Error(`Error al leer el archivo .env: ${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+
51
+ // Parsear el contenido del archivo .env
52
+ let envVars: Record<string, string>;
53
+ try {
54
+ envVars = parse(fileContent);
55
+ } catch (error) {
56
+ throw new Error(`Error al parsear el archivo .env: ${error instanceof Error ? error.message : String(error)}`);
57
+ }
58
+
59
+ // Validar cada variable
60
+ for (const [key, val] of Object.entries(envVars)) {
61
+ // Validar nombre de la variable
62
+ if (!rules.keyPattern.test(key)) {
63
+ errors.push(`Nombre inválido: "${key}" (no cumple el patrón ${rules.keyPattern})`);
64
+ continue;
65
+ }
66
+
67
+ // Validar que el valor no esté vacío
68
+ if (val == null || val === "") {
69
+ errors.push(`Variable "${key}" no tiene valor`);
70
+ continue;
71
+ }
72
+
73
+ // Validar caracteres del valor
74
+ if (!rules.valuePattern.test(val)) {
75
+ errors.push(`Valor inválido en "${key}" (contiene caracteres no permitidos)`);
76
+ continue;
77
+ }
78
+
79
+ result[key] = val;
80
+ }
81
+
82
+ if (errors.length > 0) {
83
+ throw new Error(
84
+ "Errores en variables de ambiente:\n" + errors.map(e => `- ${e}`).join("\n")
85
+ );
86
+ }
87
+
88
+ logger.info(`Loaded ${Object.keys(result).length} environment variable(s) from '${absolutePath}'`);
89
+ return result;
90
+ }
@@ -0,0 +1,17 @@
1
+ import { createLogger, format, transports } from 'winston';
2
+
3
+ const { combine, colorize, printf, timestamp } = format;
4
+
5
+ const logFormat = printf(({ level, message }) => `${level}: ${message}`);
6
+
7
+ const logger = createLogger({
8
+ level: process.env.LOG_LEVEL ?? 'info',
9
+ format: combine(colorize(), logFormat),
10
+ transports: [new transports.Console()],
11
+ });
12
+
13
+ export function setLogLevel(level: string): void {
14
+ logger.level = level;
15
+ }
16
+
17
+ export default logger;
@@ -0,0 +1,23 @@
1
+ import Table from "cli-table3";
2
+
3
+ type TablePrinterOptions<T> = {
4
+ title?: string;
5
+ headers: string[];
6
+ items: T[];
7
+ toRows: (item: T) => Array<(string | number)[]>;
8
+ footer?: string;
9
+ };
10
+
11
+ export function printTableGeneric<T>(opts: TablePrinterOptions<T>): void {
12
+ const table = new Table({
13
+ head: opts.headers,
14
+ wordWrap: true,
15
+ });
16
+ for (const item of opts.items) {
17
+ const rows = opts.toRows(item);
18
+ for (const row of rows) table.push(row);
19
+ }
20
+ if (opts.title) console.log(opts.title);
21
+ console.log(table.toString());
22
+ if (opts.footer) console.log(opts.footer);
23
+ }
@@ -0,0 +1,102 @@
1
+ import logger from '../utils/logger';
2
+ import type { ArchivedFile, ConfigMapResult } from '../types/kubernetes.types';
3
+ import { K6Config } from '../types/config.types';
4
+ import { TestRunManifest } from '../types/testRunManifest.types';
5
+
6
+ type RunnerEnvVar = { name: string; value: string };
7
+ const K6_GROUP = "k6.io";
8
+ const K6_VERSION = "v1alpha1";
9
+ const K6_KIND = "TestRun";
10
+ const K6_CLEANUP_LABEL = "post";
11
+ const ARG_OUT = "--out";
12
+ const ARG_TAG = "--tag";
13
+ const ARG_OUT_PROMETHEUS = "experimental-prometheus-rw";
14
+ const K6_PROMETHEUS_RW_SERVER_URL = "K6_PROMETHEUS_RW_SERVER_URL";
15
+ const K6_PROMETHEUS_RW_TREND_STATS = "K6_PROMETHEUS_RW_TREND_STATS";
16
+
17
+ export function buildTestRunManifest(
18
+ configMapResult: ConfigMapResult,
19
+ archiveOutput: ArchivedFile,
20
+ cfg: K6Config,
21
+ envFromLoader?: Record<string, string>
22
+ ): TestRunManifest {
23
+ const testName = configMapResult.configMapName.replace("archive-", "test-");
24
+ const argumentsString = buildArgumentsString(
25
+ cfg.arguments,
26
+ cfg,
27
+ testName
28
+ );
29
+ logger.debug("Constructed arguments string for TestRun manifest:", argumentsString);
30
+ const testRun: TestRunManifest = {
31
+ apiVersion: `${K6_GROUP}/${K6_VERSION}`,
32
+ kind: K6_KIND,
33
+ metadata: {
34
+ name: testName,
35
+ namespace: configMapResult.namespace,
36
+ },
37
+ spec: {
38
+ parallelism: cfg.parallelism,
39
+ arguments: argumentsString,
40
+ quiet: String(cfg.quiet),
41
+ ...(cfg.cleanup === true ? { cleanup: K6_CLEANUP_LABEL } : {}),
42
+ separate: cfg.separate,
43
+ runner: {
44
+ image: cfg.runner?.image,
45
+ env: buildRunnerEnv(cfg, envFromLoader),
46
+ ...(cfg.runner?.resources ? { resources: cfg.runner.resources } : {}),
47
+ },
48
+ script: {
49
+ configMap: {
50
+ name: configMapResult.configMapName,
51
+ file: archiveOutput.archiveFilename,
52
+ },
53
+ },
54
+ },
55
+ };
56
+ logger.debug("Constructed TestRun manifest:", JSON.stringify(testRun, null, 2));
57
+ return testRun;
58
+ }
59
+
60
+ function buildRunnerEnv(
61
+ cfg: K6Config,
62
+ envFromLoader?: Record<string, string>
63
+ ): RunnerEnvVar[] | undefined {
64
+ const envMap = new Map<string, string>();
65
+ if (envFromLoader) {
66
+ for (const [key, value] of Object.entries(envFromLoader)) {
67
+ if (value !== undefined && value !== null) {
68
+ envMap.set(key, String(value));
69
+ }
70
+ }
71
+ }
72
+ if (cfg.prometheus?.serverUrl) {
73
+ envMap.set(K6_PROMETHEUS_RW_SERVER_URL, cfg.prometheus.serverUrl);
74
+ if (cfg.prometheus.trendStats?.length) {
75
+ envMap.set(K6_PROMETHEUS_RW_TREND_STATS, cfg.prometheus.trendStats.join(","));
76
+ }
77
+ }
78
+ const envArray: RunnerEnvVar[] = Array.from(envMap.entries()).map(([name, value]) => ({
79
+ name,
80
+ value,
81
+ }));
82
+ return envArray.length > 0 ? envArray : undefined;
83
+ }
84
+
85
+ function buildArgumentsString(args: string[] | undefined, cfg: K6Config, name: string): string | undefined {
86
+ const finalArgs = [...(args ?? [])];
87
+ if (cfg.prometheus?.serverUrl) {
88
+ const hasOut = finalArgs.includes(ARG_OUT);
89
+ if (!hasOut) {
90
+ finalArgs.push(ARG_OUT);
91
+ }
92
+ const hasPrometheusOut = finalArgs.includes(ARG_OUT_PROMETHEUS);
93
+ if (!hasPrometheusOut) {
94
+ finalArgs.push(ARG_OUT_PROMETHEUS);
95
+ }
96
+ finalArgs.push(ARG_TAG, `testid=${name}`);
97
+ }
98
+ if (finalArgs.length === 0) {
99
+ return undefined;
100
+ }
101
+ return finalArgs.join(" ");
102
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { loadAndValidateEnv } from '../../src/utils/env';
5
+
6
+ describe("Env integration tests", () => {
7
+ test("should load and print all variables from the .env file", () => {
8
+ // create a temporary .env file before each test
9
+ const envVariables = {
10
+ API_KEY: "12345",
11
+ DB_HOST: "localhost"
12
+ };
13
+ createTempEnvFile(envVariables);
14
+ const env = loadAndValidateEnv(".env");
15
+ expect(env).toEqual(envVariables);
16
+ deleteTempEnvFile();
17
+ });
18
+ });
19
+
20
+ function createTempEnvFile(env: Record<string, string>) {
21
+ const envPath = path.resolve(__dirname, "../../.env");
22
+ const envContent = Object.entries(env)
23
+ .map(([key, value]) => `${key}=${value}`)
24
+ .join("\n");
25
+ fs.writeFileSync(envPath, envContent);
26
+ }
27
+
28
+ function deleteTempEnvFile() {
29
+ const envPath = path.resolve(__dirname, "../../.env");
30
+ if (fs.existsSync(envPath)) {
31
+ fs.unlinkSync(envPath);
32
+ }
33
+ }
@@ -0,0 +1,70 @@
1
+ import { afterAll, describe, expect, test } from '@jest/globals';
2
+ import { join, resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { unlink } from 'node:fs/promises';
5
+ import { createDefaultKubernetesService } from '../../src/services/kubernetes.service';
6
+ import { ScriptService } from '../../src/services/script.service';
7
+ import { ConfigMapResult } from '../../src/types/kubernetes.types';
8
+ import { ArchiveResult } from '../../src/types/script.types';
9
+ import logger from '../../src/utils/logger';
10
+ import { TestRunManifest } from '../../src/types/testRunManifest.types';
11
+ import { K6Config } from '../../src/types/config.types';
12
+ import { loadK6Config } from '../../src/utils/configLoader';
13
+ import { buildTestRunManifest } from '../../src/utils/testRunManifestBuilder';
14
+
15
+ const samplesPath = resolve(__dirname, '..', 'samples');
16
+ const scriptSample1 = join(samplesPath, 'k6_script_sample_1.js');
17
+ const scriptSample2 = join(samplesPath, 'k6_script_sample_2.js');
18
+ const archivedFiles: string[] = [];
19
+ const configMaps: ConfigMapResult[] = [];
20
+
21
+ const scriptService = new ScriptService();
22
+ const kubernetesService = createDefaultKubernetesService();
23
+ let archiveOutput: ArchiveResult;
24
+ let configMapResult: ConfigMapResult;
25
+
26
+ describe('KubernetesService integration tests', () => {
27
+ afterAll(async () => {
28
+ // Clean up archived files after tests
29
+ await Promise.all(
30
+ archivedFiles.map(file => unlink(file).catch(error => console.error(`Error deleting file ${file}:`, error)))
31
+ );
32
+
33
+ // Clean up created config maps in Kubernetes
34
+ await Promise.all(
35
+ configMaps.map(cm => kubernetesService.deleteConfigMap(cm.configMapName, cm.namespace)
36
+ .catch(error => console.error(`Error deleting config map ${cm.configMapName}:`, error)))
37
+ );
38
+ });
39
+
40
+ test('create config map from archived script', async () => {
41
+ archiveOutput = await scriptService.archiveTest(scriptSample2);
42
+ expect(existsSync(archiveOutput.archivePath)).toBe(true);
43
+ archivedFiles.push(archiveOutput.archivePath);
44
+
45
+ configMapResult = await kubernetesService.createConfigMap(archiveOutput, 'default');
46
+ expect(configMapResult).toBeDefined();
47
+ console.log(JSON.stringify(configMapResult, null, 2));
48
+ configMaps.push(configMapResult);
49
+ });
50
+
51
+ test('Create TestRun from the ConfigMap previously created', async () => {
52
+ const cfg: K6Config = loadK6Config();
53
+ logger.debug("Loaded K6 Config:", JSON.stringify(cfg, null, 2));
54
+ const testRunManifest: TestRunManifest = buildTestRunManifest(configMapResult, archiveOutput, cfg);
55
+ const response = await kubernetesService.createTestRun(testRunManifest);
56
+ expect(response).toBeDefined();
57
+ logger.info("TestRun result:", JSON.stringify(response));
58
+
59
+ // Wait for some time to allow the TestRun to be created and start running
60
+ await new Promise((resolve) => setTimeout(resolve, 40000));
61
+
62
+ // List TestRuns, Pods, and ConfigMaps to verify they are created and running
63
+ await kubernetesService.listTestRuns();
64
+ await kubernetesService.listPods();
65
+ await kubernetesService.listConfigMaps();
66
+
67
+ // Clean up the TestRun after successful creation and execution
68
+ await kubernetesService.deleteTestRun(testRunManifest);
69
+ }, 120000);
70
+ });
@@ -0,0 +1,64 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from '@jest/globals';
2
+ import { join, resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, rmdir, unlink } from 'node:fs/promises';
5
+ import { ScriptService } from '../../src/services/script.service';
6
+
7
+ const samplesPath = resolve(__dirname, '..', 'samples');
8
+ const scriptSample1 = join(samplesPath, 'k6_script_sample_1.js');
9
+ const scriptSample2 = join(samplesPath, 'k6_script_sample_2.js');
10
+ const archivedFiles: string[] = [];
11
+ const outputDirectory = resolve(__dirname, '..', 'archive_output');
12
+
13
+ const scriptService = new ScriptService();
14
+
15
+ describe('ScriptService integration tests', () => {
16
+ beforeAll(() => {
17
+ // Create the output directory if it doesn't exist
18
+ if (!existsSync(outputDirectory)) {
19
+ mkdir(outputDirectory, { recursive: true });
20
+ }
21
+ });
22
+
23
+ afterAll(async () => {
24
+ // Clean up archived files after tests
25
+ await Promise.all(
26
+ archivedFiles.map(file => unlink(file).catch(error => console.error(`Error deleting file ${file}:`, error)))
27
+ );
28
+ // Clean up output directory after tests
29
+ await rmdir(outputDirectory)
30
+ .catch(error => console.error(`Error deleting output directory ${outputDirectory}:`, error));
31
+ });
32
+
33
+ test('throws an error when the script does not exist', async () => {
34
+ await expect(scriptService.archiveTest('./non_existent_script.js'))
35
+ .rejects.toThrow("Script file not found at path");
36
+ });
37
+
38
+ test.skip('throws an error when k6 is not installed', async () => {
39
+ await expect(scriptService.archiveTest(scriptSample1)).rejects.toThrow("k6 is not installed");
40
+ });
41
+
42
+ test('archives the script successfully - sample #1', async () => {
43
+ const archiveOutput = await scriptService.archiveTest(scriptSample1);
44
+ expect(existsSync(archiveOutput.archivePath)).toBe(true);
45
+ archivedFiles.push(archiveOutput.archivePath);
46
+ });
47
+
48
+ test('archives the script successfully - sample #2', async () => {
49
+ const archiveOutput = await scriptService.archiveTest(scriptSample2);
50
+ expect(existsSync(archiveOutput.archivePath)).toBe(true);
51
+ archivedFiles.push(archiveOutput.archivePath);
52
+ });
53
+
54
+ test('non-existent output directory', async () => {
55
+ await expect(scriptService.archiveTest(scriptSample1, './fake-output'))
56
+ .rejects.toThrow("Output directory does not exist at path");
57
+ });
58
+
59
+ test('archives the script successfully using output directory - sample #1', async () => {
60
+ const archiveOutput = await scriptService.archiveTest(scriptSample1, outputDirectory);
61
+ expect(existsSync(archiveOutput.archivePath)).toBe(true);
62
+ archivedFiles.push(archiveOutput.archivePath);
63
+ });
64
+ });
@@ -0,0 +1,20 @@
1
+ const { createDefaultPreset } = require("ts-jest");
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ module.exports = {
7
+ testEnvironment: "node",
8
+ testMatch: [
9
+ '**/test/integration/**/*.test.ts'
10
+ ],
11
+ transform: {
12
+ ...tsJestTransformCfg,
13
+ '^.+\\.js$': ['ts-jest', {
14
+ useESM: false,
15
+ }],
16
+ },
17
+ transformIgnorePatterns: [
18
+ 'node_modules/(?!(@kubernetes/client-node|openid-client|oauth4webapi|jose))'
19
+ ],
20
+ };
@@ -0,0 +1,14 @@
1
+ const { createDefaultPreset } = require("ts-jest");
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ module.exports = {
7
+ testEnvironment: "node",
8
+ testMatch: [
9
+ '**/test/unit/**/*.test.ts'
10
+ ],
11
+ transform: {
12
+ ...tsJestTransformCfg,
13
+ },
14
+ };
@@ -0,0 +1,4 @@
1
+ foo,bar
2
+ 1,2
3
+ 3,4
4
+ 5,6
@@ -0,0 +1,9 @@
1
+ // 1. basic test script for k6, without any external dependencies
2
+
3
+ import http from 'k6/http';
4
+ import { sleep } from 'k6';
5
+
6
+ export default function () {
7
+ http.get('https://test.k6.io');
8
+ sleep(1);
9
+ }
@@ -0,0 +1,18 @@
1
+ // 2. test script for k6, loading papaparse and using it to parse a CSV file, which is stored in the SharedArray
2
+
3
+ import { sleep } from 'k6';
4
+ import { SharedArray } from 'k6/data';
5
+ import http from 'k6/http';
6
+ import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
7
+
8
+ const csvData = new SharedArray('data_sample_1', function () {
9
+ return papaparse.parse(open('./data_sample_1.csv'), { header: true }).data;
10
+ });
11
+
12
+ export default function () {
13
+ const randomIndex = Math.floor(Math.random() * csvData.length);
14
+ const randomData = csvData[randomIndex];
15
+ console.log(randomData);
16
+ http.get('https://test.k6.io');
17
+ sleep(1);
18
+ }
@@ -0,0 +1,53 @@
1
+ import { beforeEach, describe, expect, jest, test } from '@jest/globals';
2
+ import { loadK6Config } from "../../src/utils/configLoader";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+
5
+ jest.mock("node:fs", () => ({
6
+ existsSync: jest.fn(),
7
+ readFileSync: jest.fn(),
8
+ }));
9
+
10
+ const mockedExistsSync = existsSync as unknown as jest.Mock;
11
+ const mockedReadFileSync = readFileSync as unknown as jest.Mock;
12
+
13
+ describe("loadK6Config", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ test("File does not exist, returns defaults", () => {
19
+ mockedExistsSync.mockReturnValue(false);
20
+ const cfg = loadK6Config("k6ctl.config.json");
21
+ expect(cfg.namespace).toBe("default");
22
+ expect(cfg.cleanup).toBe(false);
23
+ expect(cfg.quiet).toBe(true);
24
+ expect(cfg.separate).toBe(false);
25
+ expect(cfg.parallelism).toBe(1);
26
+ expect(cfg.runner.image).toBe("grafana/k6:latest");
27
+ expect(cfg.arguments).toBeUndefined();
28
+ });
29
+
30
+ test("File exists, applies JSON + defaults", () => {
31
+ mockedExistsSync.mockReturnValue(true);
32
+ mockedReadFileSync.mockReturnValue(
33
+ JSON.stringify({
34
+ namespace: "performance",
35
+ parallelism: 40,
36
+ prometheus: { serverUrl: "http://prom-rw:9090" },
37
+ })
38
+ );
39
+ const cfg = loadK6Config("k6ctl.config.json");
40
+ expect(cfg.namespace).toBe("performance");
41
+ expect(cfg.parallelism).toBe(40);
42
+ expect(cfg.cleanup).toBe(false);
43
+ expect(cfg.runner.image).toBe("grafana/k6:latest");
44
+ expect(cfg.prometheus?.serverUrl).toBe("http://prom-rw:9090");
45
+ expect(cfg.prometheus?.trendStats).toEqual(["avg", "p(95)", "p(99)", "min", "max"]);
46
+ });
47
+
48
+ test("If JSON is invalid, throws error with clear message", () => {
49
+ mockedExistsSync.mockReturnValue(true);
50
+ mockedReadFileSync.mockReturnValue("{ invalid json");
51
+ expect(() => loadK6Config("k6ctl.config.json")).toThrow(/Config file 'k6ctl\.config\.json' exists but is not valid JSON:/i);
52
+ });
53
+ });