1
- import { registerSharedWorker , SharedWorker } from "ava/plugin"
1
+ import { registerSharedWorker } from "ava/plugin"
2
2
import hash from "object-hash"
3
3
import path from "node:path"
4
- import {
4
+ import type {
5
5
ConnectionDetailsFromWorker ,
6
- FinishedRunningBeforeTemplateIsBakedHookMessage ,
7
6
InitialWorkerData ,
8
- MessageFromWorker ,
9
- MessageToWorker ,
7
+ SharedWorkerFunctions ,
8
+ TestWorkerFunctions ,
10
9
} from "./internal-types"
11
- import {
10
+ import type {
12
11
ConnectionDetails ,
13
12
GetTestPostgresDatabase ,
14
13
GetTestPostgresDatabaseFactoryOptions ,
15
14
GetTestPostgresDatabaseOptions ,
16
- GetTestPostgresDatabaseResult ,
17
15
} from "./public-types"
18
16
import { Pool } from "pg"
19
- import { Jsonifiable } from "type-fest"
20
- import { StartedNetwork } from "testcontainers"
21
- import { ExecutionContext } from "ava"
17
+ import type { Jsonifiable } from "type-fest"
18
+ import type { ExecutionContext } from "ava"
19
+ import { once } from "node:events"
20
+ import { createBirpc } from "birpc"
21
+ import { ExecResult } from "testcontainers"
22
+ import isPlainObject from "lodash/isPlainObject"
23
+
24
+ // https://stackoverflow.com/a/30580513
25
+ const isSerializable = ( obj : Record < any , any > ) : boolean => {
26
+ var isNestedSerializable
27
+ function isPlain ( val : any ) {
28
+ return (
29
+ typeof val === "undefined" ||
30
+ typeof val === "string" ||
31
+ typeof val === "boolean" ||
32
+ typeof val === "number" ||
33
+ Array . isArray ( val ) ||
34
+ isPlainObject ( val )
35
+ )
36
+ }
37
+ if ( ! isPlain ( obj ) ) {
38
+ return false
39
+ }
40
+ for ( var property in obj ) {
41
+ if ( obj . hasOwnProperty ( property ) ) {
42
+ if ( ! isPlain ( obj [ property ] ) ) {
43
+ return false
44
+ }
45
+ if ( typeof obj [ property ] == "object" ) {
46
+ isNestedSerializable = isSerializable ( obj [ property ] )
47
+ if ( ! isNestedSerializable ) {
48
+ return false
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return true
54
+ }
22
55
23
56
const getWorker = async (
24
57
initialData : InitialWorkerData ,
@@ -50,6 +83,24 @@ const getWorker = async (
50
83
} )
51
84
}
52
85
86
+ const teardownConnection = async ( {
87
+ pool,
88
+ pgbouncerPool,
89
+ } : ConnectionDetails ) => {
90
+ try {
91
+ await pool . end ( )
92
+ await pgbouncerPool ?. end ( )
93
+ } catch ( error ) {
94
+ if (
95
+ ( error as Error ) . message . includes ( "Called end on pool more than once" )
96
+ ) {
97
+ return
98
+ }
99
+
100
+ throw error
101
+ }
102
+ }
103
+
53
104
export const getTestPostgresDatabaseFactory = <
54
105
Params extends Jsonifiable = never
55
106
> (
@@ -63,71 +114,34 @@ export const getTestPostgresDatabaseFactory = <
63
114
64
115
const workerPromise = getWorker ( initialData , options as any )
65
116
66
- const getTestPostgresDatabase : GetTestPostgresDatabase < Params > = async (
67
- t : ExecutionContext ,
68
- params : any ,
69
- getTestDatabaseOptions ?: GetTestPostgresDatabaseOptions
70
- ) => {
71
- const mapWorkerConnectionDetailsToConnectionDetails = (
72
- connectionDetailsFromWorker : ConnectionDetailsFromWorker
73
- ) : ConnectionDetails => {
74
- const pool = new Pool ( {
75
- connectionString : connectionDetailsFromWorker . connectionString ,
76
- } )
77
-
78
- let pgbouncerPool : Pool | undefined
79
- if ( connectionDetailsFromWorker . pgbouncerConnectionString ) {
80
- pgbouncerPool = new Pool ( {
81
- connectionString :
82
- connectionDetailsFromWorker . pgbouncerConnectionString ,
83
- } )
84
- }
85
-
86
- t . teardown ( async ( ) => {
87
- try {
88
- await pool . end ( )
89
- await pgbouncerPool ?. end ( )
90
- } catch ( error ) {
91
- if (
92
- ( error as Error ) . message . includes (
93
- "Called end on pool more than once"
94
- )
95
- ) {
96
- return
97
- }
117
+ const mapWorkerConnectionDetailsToConnectionDetails = (
118
+ connectionDetailsFromWorker : ConnectionDetailsFromWorker
119
+ ) : ConnectionDetails => {
120
+ const pool = new Pool ( {
121
+ connectionString : connectionDetailsFromWorker . connectionString ,
122
+ } )
98
123
99
- throw error
100
- }
124
+ let pgbouncerPool : Pool | undefined
125
+ if ( connectionDetailsFromWorker . pgbouncerConnectionString ) {
126
+ pgbouncerPool = new Pool ( {
127
+ connectionString : connectionDetailsFromWorker . pgbouncerConnectionString ,
101
128
} )
102
-
103
- return {
104
- ...connectionDetailsFromWorker ,
105
- pool,
106
- pgbouncerPool,
107
- }
108
129
}
109
130
110
- const worker = await workerPromise
111
- await worker . available
112
-
113
- const waitForAndHandleReply = async (
114
- message : SharedWorker . Plugin . PublishedMessage
115
- ) : Promise < GetTestPostgresDatabaseResult > => {
116
- let reply = await message . replies ( ) . next ( )
117
- const replyData : MessageFromWorker = reply . value . data
118
-
119
- if ( replyData . type === "RUN_HOOK_BEFORE_TEMPLATE_IS_BAKED" ) {
120
- let result : FinishedRunningBeforeTemplateIsBakedHookMessage [ "result" ] =
121
- {
122
- status : "success" ,
123
- result : undefined ,
124
- }
131
+ return {
132
+ ...connectionDetailsFromWorker ,
133
+ pool,
134
+ pgbouncerPool,
135
+ }
136
+ }
125
137
138
+ let rpcCallback : ( data : any ) => void
139
+ const rpc = createBirpc < SharedWorkerFunctions , TestWorkerFunctions > (
140
+ {
141
+ runBeforeTemplateIsBakedHook : async ( connection , params ) => {
126
142
if ( options ?. beforeTemplateIsBaked ) {
127
143
const connectionDetails =
128
- mapWorkerConnectionDetailsToConnectionDetails (
129
- replyData . connectionDetails
130
- )
144
+ mapWorkerConnectionDetailsToConnectionDetails ( connection )
131
145
132
146
// Ignore if the pool is terminated by the shared worker
133
147
// (This happens in CI for some reason even though we drain the pool first.)
@@ -143,94 +157,70 @@ export const getTestPostgresDatabaseFactory = <
143
157
throw error
144
158
} )
145
159
146
- try {
147
- const hookResult = await options . beforeTemplateIsBaked ( {
148
- params,
149
- connection : connectionDetails ,
150
- containerExec : async ( command ) => {
151
- const request = reply . value . reply ( {
152
- type : "EXEC_COMMAND_IN_CONTAINER" ,
153
- command,
154
- } )
155
-
156
- reply = await request . replies ( ) . next ( )
157
-
158
- if (
159
- reply . value . data . type !== "EXEC_COMMAND_IN_CONTAINER_RESULT"
160
- ) {
161
- throw new Error (
162
- "Expected EXEC_COMMAND_IN_CONTAINER_RESULT message"
163
- )
164
- }
165
-
166
- return reply . value . data . result
167
- } ,
168
- } )
169
-
170
- result = {
171
- status : "success" ,
172
- result : hookResult ,
173
- }
174
- } catch ( error ) {
175
- result = {
176
- status : "error" ,
177
- error :
178
- error instanceof Error
179
- ? error . stack ?? error . message
180
- : new Error (
181
- "Unknown error type thrown in beforeTemplateIsBaked hook"
182
- ) ,
183
- }
184
- } finally {
185
- // Otherwise connection will be killed by worker when converting to template
186
- await connectionDetails . pool . end ( )
187
- }
188
- }
160
+ const hookResult = await options . beforeTemplateIsBaked ( {
161
+ params : params as any ,
162
+ connection : connectionDetails ,
163
+ containerExec : async ( command ) : Promise < ExecResult > =>
164
+ rpc . execCommandInContainer ( command ) ,
165
+ } )
189
166
190
- try {
191
- return waitForAndHandleReply (
192
- reply . value . reply ( {
193
- type : "FINISHED_RUNNING_HOOK_BEFORE_TEMPLATE_IS_BAKED" ,
194
- result,
195
- } as MessageToWorker )
196
- )
197
- } catch ( error ) {
198
- if ( error instanceof Error && error . name === "DataCloneError" ) {
167
+ await teardownConnection ( connectionDetails )
168
+
169
+ if ( hookResult && ! isSerializable ( hookResult ) ) {
199
170
throw new TypeError (
200
171
"Return value of beforeTemplateIsBaked() hook could not be serialized. Make sure it returns only JSON-serializable values."
201
172
)
202
173
}
203
174
204
- throw error
205
- }
206
- } else if ( replyData . type === "GOT_DATABASE" ) {
207
- if ( replyData . beforeTemplateIsBakedResult . status === "error" ) {
208
- if ( typeof replyData . beforeTemplateIsBakedResult . error === "string" ) {
209
- throw new Error ( replyData . beforeTemplateIsBakedResult . error )
210
- }
211
-
212
- throw replyData . beforeTemplateIsBakedResult . error
175
+ return hookResult
213
176
}
177
+ } ,
178
+ } ,
179
+ {
180
+ post : async ( data ) => {
181
+ const worker = await workerPromise
182
+ await worker . available
183
+ worker . publish ( data )
184
+ } ,
185
+ on : ( data ) => {
186
+ rpcCallback = data
187
+ } ,
188
+ }
189
+ )
214
190
215
- return {
216
- ...mapWorkerConnectionDetailsToConnectionDetails (
217
- replyData . connectionDetails
218
- ) ,
219
- beforeTemplateIsBakedResult :
220
- replyData . beforeTemplateIsBakedResult . result ,
221
- }
222
- }
191
+ // Automatically cleaned up by AVA since each test file runs in a separate worker
192
+ const _messageHandlerPromise = ( async ( ) => {
193
+ const worker = await workerPromise
194
+ await worker . available
223
195
224
- throw new Error ( `Unexpected message type: ${ replyData . type } ` )
196
+ for await ( const msg of worker . subscribe ( ) ) {
197
+ rpcCallback ! ( msg . data )
225
198
}
199
+ } ) ( )
226
200
227
- return waitForAndHandleReply (
228
- worker . publish ( {
229
- type : "GET_TEST_DATABASE" ,
230
- params,
231
- key : getTestDatabaseOptions ?. databaseDedupeKey ,
232
- } as MessageToWorker )
201
+ const getTestPostgresDatabase : GetTestPostgresDatabase < Params > = async (
202
+ t : ExecutionContext ,
203
+ params : any ,
204
+ getTestDatabaseOptions ?: GetTestPostgresDatabaseOptions
205
+ ) => {
206
+ const testDatabaseConnection = await rpc . getTestDatabase ( {
207
+ databaseDedupeKey : getTestDatabaseOptions ?. databaseDedupeKey ,
208
+ params,
209
+ } )
210
+
211
+ const connectionDetails = mapWorkerConnectionDetailsToConnectionDetails (
212
+ testDatabaseConnection . connectionDetails
233
213
)
214
+
215
+ t . teardown ( async ( ) => {
216
+ await teardownConnection ( connectionDetails )
217
+ } )
218
+
219
+ return {
220
+ ...connectionDetails ,
221
+ beforeTemplateIsBakedResult :
222
+ testDatabaseConnection . beforeTemplateIsBakedResult ,
223
+ }
234
224
}
235
225
236
226
return getTestPostgresDatabase
0 commit comments