@elench/testkit 0.1.144 → 0.1.146

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.
@@ -1,28 +1,21 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { execa } from "execa";
4
3
  import {
5
4
  computeTemplateFingerprint as computeTemplateFingerprintModel,
6
5
  } from "./fingerprint.mjs";
7
6
  import {
8
- buildContainerName as buildContainerNameModel,
9
- buildDatabaseUrl as buildDatabaseUrlModel,
10
- buildRuntimeDatabaseName as buildRuntimeDatabaseNameModel,
11
- buildTemplateDatabaseName as buildTemplateDatabaseNameModel,
12
- escapeIdentifier as escapeIdentifierModel,
13
- escapeSqlLiteral as escapeSqlLiteralModel,
14
- hashString as hashStringModel,
15
- limitIdentifier as limitIdentifierModel,
16
- slugSegment as slugSegmentModel,
7
+ buildDatabaseUrl,
8
+ buildRuntimeDatabaseName,
9
+ buildTemplateDatabaseName,
10
+ hashString,
17
11
  } from "./naming.mjs";
18
12
  import {
19
- getLocalInfraDir as getLocalInfraDirModel,
20
- getLocalLocksDir as getLocalLocksDirModel,
21
- getLocalServiceCacheDir as getLocalServiceCacheDirModel,
22
- getResourceLocksDir as getResourceLocksDirModel,
23
- hasRemainingLocalArtifacts as hasRemainingLocalArtifactsModel,
24
- readStateValue as readStateValueModel,
25
- visitDirs as visitDirsModel,
13
+ getLocalInfraDir,
14
+ getLocalLocksDir,
15
+ getLocalServiceCacheDir,
16
+ getResourceLocksDir,
17
+ hasRemainingLocalArtifacts,
18
+ readStateValue,
26
19
  } from "./state.mjs";
27
20
  import {
28
21
  runTemplateStage,
@@ -37,17 +30,36 @@ import {
37
30
  verifyLocalSchemaMatchesSource,
38
31
  } from "./schema-source.mjs";
39
32
  import { collectStateDirLines } from "../runner/state-io.mjs";
33
+ import {
34
+ cloneDatabaseFromTemplate,
35
+ createEmptyDatabase,
36
+ databaseExists,
37
+ dropDatabaseIfExists,
38
+ waitForResourcePostgresReady,
39
+ } from "./admin.mjs";
40
+ import {
41
+ ensureLocalContainer,
42
+ loadExistingLocalContainer,
43
+ } from "./local-postgres.mjs";
44
+ import {
45
+ resolveResourcePostgresInfra,
46
+ resolveResourcePostgresInfraFromState,
47
+ } from "./resource-postgres.mjs";
48
+ import {
49
+ writeCacheState,
50
+ writeResourceConnectionState,
51
+ } from "./state-files.mjs";
52
+ import { withLock } from "./locks.mjs";
53
+ import {
54
+ cleanupLocalPostgresDockerResources,
55
+ } from "./cleanup.mjs";
40
56
 
41
- const LOCAL_IMAGE = "pgvector/pgvector:pg16";
42
- const LOCAL_USER = "testkit";
43
- const LOCAL_PASSWORD = "testkit";
44
- const LOCAL_ADMIN_DB = "postgres";
45
- const LOCAL_READY_TIMEOUT_MS = 60_000;
46
- const LOCAL_POLL_INTERVAL_MS = 1_000;
47
- const LOCAL_ADMIN_QUERY_RETRY_MS = 15_000;
48
- const LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS = 250;
49
- const LOCAL_DROP_DATABASE_TIMEOUT_MS = 15_000;
50
- const LOCAL_DROP_DATABASE_POLL_INTERVAL_MS = 250;
57
+ export {
58
+ dropDatabaseWithForceOrDrain,
59
+ isTransientAdminQueryConnectionError,
60
+ waitForDatabaseConnectionsToDrain,
61
+ } from "./admin.mjs";
62
+ export { formatDatabaseResourceCleanupLine } from "./cleanup.mjs";
51
63
 
52
64
  export async function prepareDatabaseRuntime(config, options = {}) {
53
65
  const db = materializeDatabaseConfig(
@@ -99,7 +111,7 @@ export async function destroyServiceDatabaseCache(productDir, serviceName) {
99
111
  const resourceLockDir = getResourceLocksDir(productDir, resourceName || "unknown");
100
112
  fs.mkdirSync(resourceLockDir, { recursive: true });
101
113
  await withLock(path.join(resourceLockDir, `template-${serviceName}.lock`), async () => {
102
- const infra = resolveResourcePostgresInfraFromState(cacheDir);
114
+ const infra = resolveResourcePostgresInfraFromState(cacheDir, readStateValue);
103
115
  const templateDbName = readStateValue(path.join(cacheDir, "template_database_name"));
104
116
  if (infra && templateDbName) {
105
117
  await dropDatabaseIfExists(infra, templateDbName);
@@ -130,15 +142,41 @@ export async function destroyServiceDatabaseCache(productDir, serviceName) {
130
142
 
131
143
  export async function cleanupOrphanedLocalInfrastructure(productDir) {
132
144
  const infraDir = getLocalInfraDir(productDir);
133
- if (!fs.existsSync(infraDir)) return;
134
-
135
- if (hasRemainingLocalArtifacts(productDir)) return;
136
145
 
137
- const containerName = readStateValue(path.join(infraDir, "container_name"));
138
- if (containerName) {
139
- await stopAndRemoveContainer(containerName);
146
+ const resourceCleanup = await cleanupOwnedLocalDatabaseResources(productDir, { stopIdle: true });
147
+ if (!hasRemainingLocalArtifacts(productDir, readStateValue)) {
148
+ fs.rmSync(infraDir, { recursive: true, force: true });
140
149
  }
141
- fs.rmSync(infraDir, { recursive: true, force: true });
150
+ return resourceCleanup;
151
+ }
152
+
153
+ export async function destroyOwnedLocalDatabaseResources(productDir, options = {}) {
154
+ return cleanupOwnedLocalDatabaseResources(productDir, {
155
+ ...options,
156
+ force: true,
157
+ });
158
+ }
159
+
160
+ export async function cleanupOwnedLocalDatabaseResources(productDir, options = {}) {
161
+ return cleanupLocalPostgresDockerResources({
162
+ productDir,
163
+ dryRun: Boolean(options.dryRun),
164
+ force: Boolean(options.force),
165
+ global: false,
166
+ includeLegacy: Boolean(options.includeLegacy),
167
+ stopIdle: options.stopIdle !== false,
168
+ });
169
+ }
170
+
171
+ export async function cleanupGlobalLocalDatabaseResources(options = {}) {
172
+ return cleanupLocalPostgresDockerResources({
173
+ productDir: options.productDir || process.cwd(),
174
+ dryRun: Boolean(options.dryRun),
175
+ force: false,
176
+ global: true,
177
+ includeLegacy: Boolean(options.includeLegacy),
178
+ stopIdle: options.stopIdle !== false,
179
+ });
142
180
  }
143
181
 
144
182
  export function isDatabaseStateDir(dir) {
@@ -449,565 +487,11 @@ async function destroyResourceRuntimeDatabase(stateDir) {
449
487
  const dbName = readStateValue(path.join(stateDir, "resource_database_name"));
450
488
  if (!dbName) return;
451
489
 
452
- const infra = resolveResourcePostgresInfraFromState(stateDir);
490
+ const infra = resolveResourcePostgresInfraFromState(stateDir, readStateValue);
453
491
  if (!infra) return;
454
492
  await dropDatabaseIfExists(infra, dbName);
455
493
  }
456
494
 
457
- async function ensureLocalContainer(productDir, database = {}) {
458
- const infraDir = getLocalInfraDir(productDir);
459
- fs.mkdirSync(infraDir, { recursive: true });
460
-
461
- const containerName =
462
- readStateValue(path.join(infraDir, "container_name")) || buildContainerName(productDir);
463
- const image = database.image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE;
464
- const user = database.user || readStateValue(path.join(infraDir, "user")) || LOCAL_USER;
465
- const password =
466
- database.password || readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD;
467
-
468
- let inspect = await inspectContainer(containerName);
469
- if (inspect && inspect.Config?.Image !== image) {
470
- await stopAndRemoveContainer(containerName);
471
- inspect = null;
472
- }
473
-
474
- if (!inspect) {
475
- await execa("docker", [
476
- "run",
477
- "-d",
478
- "--name",
479
- containerName,
480
- "-e",
481
- `POSTGRES_USER=${user}`,
482
- "-e",
483
- `POSTGRES_PASSWORD=${password}`,
484
- "-e",
485
- `POSTGRES_DB=${LOCAL_ADMIN_DB}`,
486
- "-p",
487
- "127.0.0.1::5432",
488
- image,
489
- ]);
490
- inspect = await inspectContainer(containerName);
491
- } else if (!inspect.State?.Running) {
492
- await execa("docker", ["start", containerName]);
493
- inspect = await inspectContainer(containerName);
494
- }
495
-
496
- const hostPort = inspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
497
- if (!hostPort) {
498
- throw new Error(`Could not determine published port for local database container ${containerName}`);
499
- }
500
-
501
- const infra = {
502
- containerName,
503
- containerId: inspect.Id,
504
- image,
505
- user,
506
- password,
507
- host: "127.0.0.1",
508
- port: Number(hostPort),
509
- };
510
-
511
- await waitForLocalContainerReady(infra);
512
- writeLocalInfraState(infraDir, infra);
513
- return infra;
514
- }
515
-
516
- async function loadExistingLocalContainer(productDir) {
517
- const infraDir = getLocalInfraDir(productDir);
518
- const containerName = readStateValue(path.join(infraDir, "container_name"));
519
- if (!containerName) return null;
520
-
521
- const inspect = await inspectContainer(containerName);
522
- if (!inspect) return null;
523
-
524
- if (!inspect.State?.Running) {
525
- await execa("docker", ["start", containerName]);
526
- }
527
-
528
- const nextInspect = await inspectContainer(containerName);
529
- const hostPort = nextInspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
530
- if (!hostPort) return null;
531
-
532
- const infra = {
533
- containerName,
534
- containerId: nextInspect.Id,
535
- image: nextInspect.Config?.Image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE,
536
- user: readStateValue(path.join(infraDir, "user")) || LOCAL_USER,
537
- password: readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD,
538
- host: "127.0.0.1",
539
- port: Number(hostPort),
540
- };
541
- await waitForLocalContainerReady(infra);
542
- return infra;
543
- }
544
-
545
- async function waitForLocalContainerReady(infra) {
546
- const startedAt = Date.now();
547
- while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
548
- try {
549
- await execa("docker", [
550
- "exec",
551
- infra.containerName,
552
- "pg_isready",
553
- "-U",
554
- infra.user,
555
- "-d",
556
- LOCAL_ADMIN_DB,
557
- ]);
558
- return;
559
- } catch {
560
- await sleep(LOCAL_POLL_INTERVAL_MS);
561
- }
562
- }
563
-
564
- throw new Error(`Timed out waiting for local database container ${infra.containerName}`);
565
- }
566
-
567
- async function waitForResourcePostgresReady(infra) {
568
- const startedAt = Date.now();
569
- while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
570
- try {
571
- await runAdminQuery(infra, ["-tAc", "SELECT 1"]);
572
- return;
573
- } catch (error) {
574
- if (!isTransientAdminQueryConnectionError(error)) throw error;
575
- await sleep(LOCAL_POLL_INTERVAL_MS);
576
- }
577
- }
578
-
579
- throw new Error(`Timed out waiting for Postgres resource "${infra.resourceName}" at ${infra.host}:${infra.port}`);
580
- }
581
-
582
- async function inspectContainer(containerName) {
583
- try {
584
- const { stdout } = await execa("docker", ["inspect", containerName]);
585
- const parsed = JSON.parse(stdout);
586
- return parsed[0] || null;
587
- } catch {
588
- return null;
589
- }
590
- }
591
-
592
- async function stopAndRemoveContainer(containerName) {
593
- try {
594
- await execa("docker", ["rm", "-f", containerName]);
595
- } catch {
596
- // Already gone.
597
- }
598
- }
599
-
600
- async function databaseExists(infra, dbName) {
601
- const result = await runAdminQuery(infra, [
602
- "-tAc",
603
- `SELECT 1 FROM pg_database WHERE datname = '${escapeSqlLiteral(dbName)}'`,
604
- ]);
605
- return result.trim() === "1";
606
- }
607
-
608
- async function createEmptyDatabase(infra, dbName) {
609
- await runAdminQuery(infra, ["-c", `CREATE DATABASE "${escapeIdentifier(dbName)}"`]);
610
- }
611
-
612
- async function cloneDatabaseFromTemplate(infra, dbName, templateDbName) {
613
- await runAdminQuery(infra, [
614
- "-c",
615
- `CREATE DATABASE "${escapeIdentifier(dbName)}" TEMPLATE "${escapeIdentifier(templateDbName)}"`,
616
- ]);
617
- }
618
-
619
- async function dropDatabaseIfExists(infra, dbName) {
620
- await dropDatabaseWithForceOrDrain(infra, dbName);
621
- }
622
-
623
- export async function dropDatabaseWithForceOrDrain(infra, dbName, hooks = {}) {
624
- const runAdminQueryFn = hooks.runAdminQuery || runAdminQuery;
625
- const databaseExistsFn = hooks.databaseExists || databaseExists;
626
- const sleepFn = hooks.sleep || sleep;
627
-
628
- try {
629
- await runAdminQueryFn(infra, [
630
- "-c",
631
- `DROP DATABASE IF EXISTS "${escapeIdentifier(dbName)}" WITH (FORCE)`,
632
- ]);
633
- return;
634
- } catch (error) {
635
- if (!isUnsupportedForceDropError(error)) {
636
- throw error;
637
- }
638
- }
639
-
640
- if (!(await databaseExistsFn(infra, dbName))) {
641
- return;
642
- }
643
-
644
- let restoreConnections = false;
645
- try {
646
- await runAdminQueryFn(infra, [
647
- "-c",
648
- `ALTER DATABASE "${escapeIdentifier(dbName)}" WITH ALLOW_CONNECTIONS false`,
649
- ]);
650
- restoreConnections = true;
651
- await waitForDatabaseConnectionsToDrain(infra, dbName, {
652
- runAdminQuery: runAdminQueryFn,
653
- sleep: sleepFn,
654
- });
655
- await runAdminQueryFn(infra, [
656
- "-c",
657
- `DROP DATABASE IF EXISTS "${escapeIdentifier(dbName)}"`,
658
- ]);
659
- restoreConnections = false;
660
- } finally {
661
- if (restoreConnections && (await databaseExistsFn(infra, dbName))) {
662
- await runAdminQueryFn(infra, [
663
- "-c",
664
- `ALTER DATABASE "${escapeIdentifier(dbName)}" WITH ALLOW_CONNECTIONS true`,
665
- ]).catch(() => {
666
- // Best-effort restoration for failed fallback drops.
667
- });
668
- }
669
- }
670
- }
671
-
672
- export async function waitForDatabaseConnectionsToDrain(infra, dbName, hooks = {}) {
673
- const runAdminQueryFn = hooks.runAdminQuery || runAdminQuery;
674
- const sleepFn = hooks.sleep || sleep;
675
- const now = hooks.now || Date.now;
676
- const timeoutMs = hooks.timeoutMs ?? LOCAL_DROP_DATABASE_TIMEOUT_MS;
677
- const deadline = now() + timeoutMs;
678
-
679
- while (true) {
680
- await terminateDatabaseConnections(infra, dbName, runAdminQueryFn);
681
- const remainingConnections = await countDatabaseConnections(infra, dbName, runAdminQueryFn);
682
- if (remainingConnections === 0) {
683
- return;
684
- }
685
- if (now() >= deadline) {
686
- throw new Error(
687
- `Timed out waiting for database "${dbName}" connections to close (${remainingConnections} remaining)`
688
- );
689
- }
690
- await sleepFn(LOCAL_DROP_DATABASE_POLL_INTERVAL_MS);
691
- }
692
- }
693
-
694
- async function runAdminQuery(infra, args) {
695
- const command = infra.containerName ? "docker" : "psql";
696
- const commandArgs = infra.containerName
697
- ? [
698
- "exec",
699
- "-e",
700
- `PGPASSWORD=${infra.password}`,
701
- infra.containerName,
702
- "psql",
703
- "-v",
704
- "ON_ERROR_STOP=1",
705
- "-U",
706
- infra.user,
707
- "-d",
708
- infra.adminDatabase || LOCAL_ADMIN_DB,
709
- ...args,
710
- ]
711
- : [
712
- "-v",
713
- "ON_ERROR_STOP=1",
714
- "-h",
715
- infra.host,
716
- "-p",
717
- String(infra.port),
718
- "-U",
719
- infra.user,
720
- "-d",
721
- infra.adminDatabase || LOCAL_ADMIN_DB,
722
- ...args,
723
- ];
724
- const commandOptions = infra.containerName
725
- ? {}
726
- : {
727
- env: {
728
- ...process.env,
729
- PGPASSWORD: infra.password,
730
- ...(infra.sslMode ? { PGSSLMODE: infra.sslMode } : {}),
731
- },
732
- };
733
- const startedAt = Date.now();
734
- while (true) {
735
- try {
736
- const { stdout } = await execa(command, commandArgs, commandOptions);
737
- return stdout;
738
- } catch (error) {
739
- if (
740
- Date.now() - startedAt >= LOCAL_ADMIN_QUERY_RETRY_MS ||
741
- !isTransientAdminQueryConnectionError(error)
742
- ) {
743
- throw error;
744
- }
745
- await sleep(LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS);
746
- }
747
- }
748
- }
749
-
750
- export function isTransientAdminQueryConnectionError(error) {
751
- const text = `${error?.shortMessage || ""}\n${error?.stderr || ""}\n${error?.stdout || ""}\n${error?.message || ""}`;
752
- return (
753
- text.includes('connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed') ||
754
- text.includes("No such file or directory") ||
755
- text.includes("the database system is starting up") ||
756
- text.includes("could not connect to server")
757
- );
758
- }
759
-
760
- async function terminateDatabaseConnections(infra, dbName, runAdminQueryFn) {
761
- await runAdminQueryFn(infra, [
762
- "-c",
763
- [
764
- "SELECT pg_terminate_backend(pid)",
765
- "FROM pg_stat_activity",
766
- `WHERE datname = '${escapeSqlLiteral(dbName)}' AND pid <> pg_backend_pid();`,
767
- ].join(" "),
768
- ]);
769
- }
770
-
771
- async function countDatabaseConnections(infra, dbName, runAdminQueryFn) {
772
- const result = await runAdminQueryFn(infra, [
773
- "-tAc",
774
- [
775
- "SELECT COUNT(*)",
776
- "FROM pg_stat_activity",
777
- `WHERE datname = '${escapeSqlLiteral(dbName)}' AND pid <> pg_backend_pid();`,
778
- ].join(" "),
779
- ]);
780
- return Number.parseInt(result.trim(), 10) || 0;
781
- }
782
-
783
- function isUnsupportedForceDropError(error) {
784
- const text = `${error?.shortMessage || ""}\n${error?.stderr || ""}\n${error?.stdout || ""}\n${error?.message || ""}`;
785
- return (
786
- text.includes('syntax error at or near "WITH"') ||
787
- text.includes('option "force"')
788
- );
789
- }
790
-
791
495
  async function computeTemplateFingerprint(config, options = {}) {
792
496
  return computeTemplateFingerprintModel(config, options);
793
497
  }
794
-
795
- function buildDatabaseUrl(infra, dbName) {
796
- return buildDatabaseUrlModel(infra, dbName);
797
- }
798
-
799
- function resolveResourcePostgresInfra(config, processEnv = process.env) {
800
- const resourceName = String(config.testkit?.database?.resource || "").trim();
801
- if (!resourceName) {
802
- throw new Error("Resource-backed Postgres database requires database.resource");
803
- }
804
- const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
805
- const connection = connections[resourceName];
806
- if (!connection) {
807
- const available = Object.keys(connections).sort().join(", ") || "none";
808
- throw new Error(`Postgres resource "${resourceName}" is not available. Available resources: ${available}`);
809
- }
810
- return normalizeResourcePostgresConnection(resourceName, connection);
811
- }
812
-
813
- function resolveResourcePostgresInfraFromState(stateDir, processEnv = process.env) {
814
- const resourceName = readStateValue(path.join(stateDir, "resource_name"));
815
- if (!resourceName) return null;
816
- const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
817
- const connection = connections[resourceName] || readResourceConnectionState(stateDir);
818
- return connection ? normalizeResourcePostgresConnection(resourceName, connection) : null;
819
- }
820
-
821
- function parseResourceConnections(raw) {
822
- if (!raw) return {};
823
- try {
824
- const parsed = JSON.parse(raw);
825
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
826
- } catch (error) {
827
- throw new Error(`Invalid TESTKIT_RESOURCE_CONNECTIONS_JSON: ${error.message}`);
828
- }
829
- }
830
-
831
- function normalizeResourcePostgresConnection(resourceName, connection) {
832
- const fromUrl = connection.url ? parsePostgresConnectionUrl(connection.url) : {};
833
- const host = connection.host || fromUrl.host;
834
- const port = Number(connection.port || fromUrl.port || 5432);
835
- const user = connection.user || fromUrl.user;
836
- const password = connection.password || fromUrl.password || "";
837
- const adminDatabase = connection.adminDatabase || connection.admin_database || fromUrl.database || LOCAL_ADMIN_DB;
838
- const sslMode = connection.sslMode || connection.sslmode || fromUrl.sslMode || "disable";
839
- if (!host) throw new Error(`Postgres resource "${resourceName}" connection is missing host`);
840
- if (!Number.isInteger(port) || port <= 0) {
841
- throw new Error(`Postgres resource "${resourceName}" connection has invalid port`);
842
- }
843
- if (!user) throw new Error(`Postgres resource "${resourceName}" connection is missing user`);
844
- return {
845
- backend: "resource",
846
- resourceName,
847
- host,
848
- port,
849
- user,
850
- password,
851
- adminDatabase,
852
- sslMode,
853
- };
854
- }
855
-
856
- function parsePostgresConnectionUrl(rawUrl) {
857
- const parsed = new URL(rawUrl);
858
- return {
859
- host: parsed.hostname,
860
- port: parsed.port ? Number(parsed.port) : 5432,
861
- database: decodeURIComponent(parsed.pathname.replace(/^\//, "")) || LOCAL_ADMIN_DB,
862
- user: decodeURIComponent(parsed.username || ""),
863
- password: decodeURIComponent(parsed.password || ""),
864
- sslMode: parsed.searchParams.get("sslmode") || undefined,
865
- };
866
- }
867
-
868
- function buildContainerName(productDir) {
869
- return buildContainerNameModel(productDir);
870
- }
871
-
872
- function buildTemplateDatabaseName(serviceName, fingerprint) {
873
- return buildTemplateDatabaseNameModel(serviceName, fingerprint);
874
- }
875
-
876
- function buildRuntimeDatabaseName(serviceName, bindingKey, fingerprint) {
877
- return buildRuntimeDatabaseNameModel(serviceName, bindingKey, fingerprint);
878
- }
879
-
880
- function writeLocalInfraState(infraDir, infra) {
881
- fs.writeFileSync(path.join(infraDir, "database_backend"), "local");
882
- fs.writeFileSync(path.join(infraDir, "container_name"), infra.containerName);
883
- fs.writeFileSync(path.join(infraDir, "container_id"), infra.containerId);
884
- fs.writeFileSync(path.join(infraDir, "image"), infra.image);
885
- fs.writeFileSync(path.join(infraDir, "user"), infra.user);
886
- fs.writeFileSync(path.join(infraDir, "password"), infra.password);
887
- fs.writeFileSync(path.join(infraDir, "host"), infra.host);
888
- fs.writeFileSync(path.join(infraDir, "port"), String(infra.port));
889
- }
890
-
891
- function writeCacheState(cacheDir, config, infra, templateDbName, fingerprint) {
892
- const backend = config.testkit.database.provider;
893
- fs.writeFileSync(path.join(cacheDir, "database_backend"), backend);
894
- fs.writeFileSync(path.join(cacheDir, "template_database_name"), templateDbName);
895
- fs.writeFileSync(path.join(cacheDir, "template_fingerprint"), fingerprint);
896
- if (backend === "local") {
897
- fs.writeFileSync(path.join(cacheDir, "container_name"), infra.containerName);
898
- } else {
899
- fs.writeFileSync(path.join(cacheDir, "resource_name"), infra.resourceName);
900
- writeResourceConnectionState(cacheDir, infra);
901
- }
902
- }
903
-
904
- function writeResourceConnectionState(stateDir, infra) {
905
- fs.writeFileSync(path.join(stateDir, "resource_host"), infra.host);
906
- fs.writeFileSync(path.join(stateDir, "resource_port"), String(infra.port));
907
- fs.writeFileSync(path.join(stateDir, "resource_user"), infra.user);
908
- fs.writeFileSync(path.join(stateDir, "resource_password"), infra.password);
909
- fs.writeFileSync(path.join(stateDir, "resource_admin_database"), infra.adminDatabase || LOCAL_ADMIN_DB);
910
- fs.writeFileSync(path.join(stateDir, "resource_sslmode"), infra.sslMode || "disable");
911
- }
912
-
913
- function readResourceConnectionState(stateDir) {
914
- const host = readStateValue(path.join(stateDir, "resource_host"));
915
- const user = readStateValue(path.join(stateDir, "resource_user"));
916
- if (!host || !user) return null;
917
- return {
918
- host,
919
- port: Number(readStateValue(path.join(stateDir, "resource_port")) || 5432),
920
- user,
921
- password: readStateValue(path.join(stateDir, "resource_password")) || "",
922
- adminDatabase: readStateValue(path.join(stateDir, "resource_admin_database")) || LOCAL_ADMIN_DB,
923
- sslMode: readStateValue(path.join(stateDir, "resource_sslmode")) || "disable",
924
- };
925
- }
926
-
927
- function getLocalInfraDir(productDir) {
928
- return getLocalInfraDirModel(productDir);
929
- }
930
-
931
- function getLocalLocksDir(productDir) {
932
- return getLocalLocksDirModel(productDir);
933
- }
934
-
935
- function getLocalServiceCacheDir(productDir, serviceName) {
936
- return getLocalServiceCacheDirModel(productDir, serviceName);
937
- }
938
-
939
- function getResourceLocksDir(productDir, resourceName) {
940
- return getResourceLocksDirModel(productDir, resourceName);
941
- }
942
-
943
- function hasRemainingLocalArtifacts(productDir) {
944
- return hasRemainingLocalArtifactsModel(productDir, readStateValue);
945
- }
946
-
947
- function visitDirs(root, visitor) {
948
- return visitDirsModel(root, visitor);
949
- }
950
-
951
- async function withLock(lockPath, fn) {
952
- const lockDir = `${lockPath}.dir`;
953
- const timeoutMs = 60_000;
954
- const startedAt = Date.now();
955
-
956
- while (true) {
957
- try {
958
- fs.mkdirSync(lockDir, { recursive: false });
959
- break;
960
- } catch (error) {
961
- if (error.code !== "EEXIST") throw error;
962
- if (Date.now() - startedAt > timeoutMs) {
963
- throw new Error(`Timed out waiting for lock ${path.basename(lockPath)}`);
964
- }
965
- await sleep(200);
966
- }
967
- }
968
-
969
- try {
970
- return await fn();
971
- } finally {
972
- fs.rmSync(lockDir, { recursive: true, force: true });
973
- }
974
- }
975
-
976
- function hashString(value, length = 12) {
977
- return hashStringModel(value, length);
978
- }
979
-
980
- function slugSegment(value) {
981
- return slugSegmentModel(value);
982
- }
983
-
984
- function limitIdentifier(value, maxLength) {
985
- return limitIdentifierModel(value, maxLength);
986
- }
987
-
988
- function escapeIdentifier(value) {
989
- return escapeIdentifierModel(value);
990
- }
991
-
992
- function escapeSqlLiteral(value) {
993
- return escapeSqlLiteralModel(value);
994
- }
995
-
996
- function readStateValue(filePath) {
997
- return readStateValueModel(filePath);
998
- }
999
-
1000
- function sleep(ms) {
1001
- return new Promise((resolve) => setTimeout(resolve, ms));
1002
- }
1003
-
1004
- function removeLocalRuntimeState(stateDir) {
1005
- for (const file of [
1006
- "local_database_name",
1007
- "local_template_fingerprint",
1008
- "local_template_database_name",
1009
- "local_container_name",
1010
- ]) {
1011
- fs.rmSync(path.join(stateDir, file), { force: true });
1012
- }
1013
- }