@@ -5,6 +5,8 @@ import { RedisOptions } from "ioredis";
55import { Network , type StartedNetwork } from "testcontainers" ;
66import { test } from "vitest" ;
77import { createElectricContainer , createPostgresContainer , createRedisContainer } from "./utils" ;
8+ import { x } from "tinyexec" ;
9+ import { isCI } from "std-env" ;
810
911export { assertNonNullable } from "./utils" ;
1012export { StartedRedisContainer } ;
@@ -45,31 +47,177 @@ async function logCleanup(resource: string, promise: Promise<unknown>) {
4547 const activeAtStart = ++ activeCleanups ;
4648
4749 let error : unknown = null ;
50+
4851 try {
4952 await promise ;
5053 } catch ( err ) {
5154 error = err instanceof Error ? err . message : String ( err ) ;
5255 }
5356
5457 const end = new Date ( ) ;
58+ const durationMs = end . getTime ( ) - start . getTime ( ) ;
5559 const activeAtEnd = -- activeCleanups ;
5660 const parallel = activeAtStart > 1 || activeAtEnd > 0 ;
5761
62+ if ( ! isCI ) {
63+ return ;
64+ }
65+
66+ let dockerDiagnostics : DockerDiagnostics = { } ;
67+
68+ // Only run docker diagnostics if there was an error or cleanup took longer than 5s
69+ if ( error || durationMs > 5000 ) {
70+ try {
71+ dockerDiagnostics = await getDockerDiagnostics ( ) ;
72+ } catch ( diagnosticErr ) {
73+ console . error ( "Failed to get docker diagnostics:" , diagnosticErr ) ;
74+ }
75+ }
76+
5877 console . log (
5978 JSON . stringify ( {
6079 order,
6180 resource,
62- durationMs : end . getTime ( ) - start . getTime ( ) ,
81+ durationMs,
6382 start : start . toISOString ( ) ,
6483 end : end . toISOString ( ) ,
6584 parallel,
6685 error,
6786 activeAtStart,
6887 activeAtEnd,
88+ ...dockerDiagnostics ,
6989 } )
7090 ) ;
7191}
7292
93+ function stringToLines ( str : string ) : string [ ] {
94+ return str . split ( "\n" ) . filter ( Boolean ) ;
95+ }
96+
97+ async function getDockerNetworks ( ) : Promise < string [ ] > {
98+ try {
99+ const result = await x ( "docker" , [ "network" , "ls" /* , "--no-trunc" */ ] ) ;
100+ return stringToLines ( result . stdout ) ;
101+ } catch ( error ) {
102+ console . error ( error ) ;
103+ return [ "error: check additional logs for more details" ] ;
104+ }
105+ }
106+
107+ async function getDockerContainers ( ) : Promise < string [ ] > {
108+ try {
109+ const result = await x ( "docker" , [ "ps" , "-a" /* , "--no-trunc" */ ] ) ;
110+ return stringToLines ( result . stdout ) ;
111+ } catch ( error ) {
112+ console . error ( error ) ;
113+ return [ "error: check additional logs for more details" ] ;
114+ }
115+ }
116+
117+ type DockerNetworkAttachment = {
118+ networkId : string ;
119+ networkName : string ;
120+ containers : string [ ] ;
121+ } ;
122+
123+ export async function getDockerNetworkAttachments ( ) : Promise < DockerNetworkAttachment [ ] > {
124+ let attachments : DockerNetworkAttachment [ ] = [ ] ;
125+ let networkIds : string [ ] = [ ] ;
126+
127+ try {
128+ const result = await x ( "docker" , [ "network" , "ls" , "-q" ] ) ;
129+ networkIds = stringToLines ( result . stdout ) ;
130+ } catch ( err ) {
131+ console . error ( "Failed to list docker networks:" , err ) ;
132+ }
133+
134+ for ( const networkId of networkIds ) {
135+ try {
136+ const inspectResult = await x ( "docker" , [
137+ "network" ,
138+ "inspect" ,
139+ "--format" ,
140+ '{{ .Name }}{{ range $k, $v := .Containers }} {{ printf "%.12s %s" $k .Name }}{{ end }}' ,
141+ networkId ,
142+ ] ) ;
143+
144+ const [ networkName , ...containers ] = inspectResult . stdout . trim ( ) . split ( / \s + / ) ;
145+ attachments . push ( { networkId, networkName, containers } ) ;
146+ } catch ( err ) {
147+ console . error ( `Failed to inspect network ${ networkId } :` , err ) ;
148+ attachments . push ( { networkId, networkName : String ( err ) , containers : [ ] } ) ;
149+ }
150+ }
151+
152+ return attachments ;
153+ }
154+
155+ type DockerContainerNetwork = {
156+ containerId : string ;
157+ containerName : string ;
158+ networks : string [ ] ;
159+ } ;
160+
161+ export async function getDockerContainerNetworks ( ) : Promise < DockerContainerNetwork [ ] > {
162+ let results : DockerContainerNetwork [ ] = [ ] ;
163+ let containers : string [ ] = [ ] ;
164+
165+ try {
166+ const result = await x ( "docker" , [
167+ "ps" ,
168+ "-a" ,
169+ "--format" ,
170+ '{{.ID | printf "%.12s"}} {{.Names}}' ,
171+ ] ) ;
172+ containers = stringToLines ( result . stdout ) ;
173+ } catch ( err ) {
174+ console . error ( "Failed to list docker containers:" , err ) ;
175+ }
176+
177+ for ( const [ containerId , containerName ] of containers . map ( ( c ) => c . trim ( ) . split ( / \s + / ) ) ) {
178+ try {
179+ const inspectResult = await x ( "docker" , [
180+ "inspect" ,
181+ "--format" ,
182+ "{{ range $k, $v := .NetworkSettings.Networks }}{{ $k }}{{ end }}" ,
183+ containerId ,
184+ ] ) ;
185+
186+ const networks = inspectResult . stdout . trim ( ) . split ( / \s + / ) ;
187+
188+ results . push ( { containerId, containerName, networks } ) ;
189+ } catch ( err ) {
190+ console . error ( `Failed to inspect container ${ containerId } :` , err ) ;
191+ results . push ( { containerId, containerName : String ( err ) , networks : [ ] } ) ;
192+ }
193+ }
194+
195+ return results ;
196+ }
197+
198+ type DockerDiagnostics = {
199+ containers ?: string [ ] ;
200+ networks ?: string [ ] ;
201+ containerNetworks ?: DockerContainerNetwork [ ] ;
202+ networkAttachments ?: DockerNetworkAttachment [ ] ;
203+ } ;
204+
205+ async function getDockerDiagnostics ( ) : Promise < DockerDiagnostics > {
206+ const [ containers , networks , networkAttachments , containerNetworks ] = await Promise . all ( [
207+ getDockerContainers ( ) ,
208+ getDockerNetworks ( ) ,
209+ getDockerNetworkAttachments ( ) ,
210+ getDockerContainerNetworks ( ) ,
211+ ] ) ;
212+
213+ return {
214+ containers,
215+ networks,
216+ containerNetworks,
217+ networkAttachments,
218+ } ;
219+ }
220+
73221const network = async ( { } , use : Use < StartedNetwork > ) => {
74222 const network = await new Network ( ) . start ( ) ;
75223 try {
0 commit comments