1+
12const cp = require ( 'child_process' ) ;
23const path = require ( 'path' ) ;
34const fetch = require ( 'node-fetch' ) ;
45const { expect} = require ( 'chai' ) ;
56const { v4 : uuidv4 } = require ( 'uuid' ) ;
6- let testFlag = true ;
7- let uniqueID ;
8- let externalIP ;
7+
8+ // Configuration Constants
9+ const GCP_ZONE = 'us-central1-f' ;
10+ const IMAGE_FAMILY = 'debian-12' ;
11+ const IMAGE_PROJECT = 'debian-cloud' ;
12+ const MACHINE_TYPE = 'g1-small' ;
13+ const APP_PORT = '8080' ;
14+ const STARTUP_SCRIPT_PATH = 'gce/startup-script.sh' ; // Relative to project root
15+ const MAX_PING_ATTEMPTS = 10 ;
16+ const INITIAL_PING_DELAY_SECONDS = 2 ;
917
1018async function pingVMExponential ( address , count ) {
11- await new Promise ( ( r ) => setTimeout ( r , Math . pow ( 2 , count ) * 1000 ) ) ;
19+ if ( attempt > MAX_PING_ATTEMPTS ) {
20+ throw new Error ( `Failed to connect to ${ address } after ${ MAX_PING_ATTEMPTS } attempts.` ) ;
21+ }
22+ const delaySeconds = Math . pow ( INITIAL_PING_DELAY_SECONDS , attempt - 1 ) ; // Start with 1s, then 2s, 4s, 8s etc.
23+ console . log ( `Ping attempt ${ attempt } /${ MAX_PING_ATTEMPTS } : Waiting ${ delaySeconds } s before pinging ${ address } ...` ) ;
24+ await new Promise ( ( r ) => setTimeout ( r , delaySeconds * 1000 ) ) ;
25+
1226 try {
13- const res = await fetch ( address ) ;
27+ const res = await fetch ( address , { timeout : 15000 } ) ; // Add a timeout to fetch itself
1428 if ( res . status !== 200 ) {
15- throw new Error ( res . status ) ;
29+ console . warn ( `Ping attempt ${ attempt } to ${ address } failed with status: ${ res . status } ` ) ;
30+ throw new Error ( `Status: ${ res . status } ` ) ;
1631 }
32+ console . log ( `Successfully connected to ${ address } on attempt ${ attempt } .` ) ;
33+ return true ;
1734 } catch ( err ) {
1835 process . stdout . write ( '.' ) ;
19- await pingVMExponential ( address , ++ count ) ;
36+ if ( attempt >= MAX_PING_ATTEMPTS ) {
37+ console . error ( `\nFinal ping attempt to ${ address } failed: ${ err . message } ` ) ;
38+ throw err ; // Re-throw the error if max attempts reached
39+ }
40+ // Log the error for the current attempt but continue to retry
41+ // console.warn(`Ping attempt ${attempt} to ${address} caught error: ${err.message}. Retrying...`);
42+ return pingVMExponential ( address , attempt + 1 ) ;
2043 }
2144}
2245
23- async function getIP ( uniqueID ) {
24- externalIP = cp
25- . execSync (
26- `gcloud compute instances describe my-app-instance-${ uniqueID } \
27- --format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=us-central1-f`
28- )
29- . toString ( 'utf8' )
30- . trim ( ) ;
31-
32- await pingVMExponential ( `http://${ externalIP } :8080/` , 1 ) ;
46+ async function getExternalIP ( instanceName , zone ) {
47+ try {
48+ // Retry a few times as IP address might take a moment to appear after instance is "RUNNING"
49+ for ( let i = 0 ; i < 5 ; i ++ ) {
50+ const ip = cp
51+ . execSync (
52+ `gcloud compute instances describe ${ instanceName } --format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=${ zone } `
53+ )
54+ . toString ( 'utf8' )
55+ . trim ( ) ;
56+ if ( ip ) return ip ;
57+ console . log ( `Attempt ${ i + 1 } to get IP for ${ instanceName } : IP not found yet. Waiting 5s...` ) ;
58+ await new Promise ( resolve => setTimeout ( resolve , 5000 ) ) ;
59+ }
60+ throw new Error ( `Could not retrieve external IP for ${ instanceName } after multiple attempts.` ) ;
61+ } catch ( error ) {
62+ console . error ( `Error getting external IP for ${ instanceName } :` , error . message ) ;
63+ throw error ; // Re-throw to fail the calling function (e.g., before hook)
64+ }
3365}
3466
35- describe ( 'spin up gce instance' , async function ( ) {
36- console . time ( 'beforeHook' ) ;
37- console . time ( 'test' ) ;
38- console . time ( 'afterHook' ) ;
39- this . timeout ( 250000 ) ;
40- uniqueID = uuidv4 ( ) . split ( '-' ) [ 0 ] ;
67+ describe ( 'spin up gce instance' , function ( ) {
68+ // Increase timeout for the whole describe block if necessary,
69+ // but individual hooks/tests have their own timeouts.
70+ this . timeout ( 300000 ) ; // e.g., 5 minutes for the whole suite
71+
72+ let uniqueID ;
73+ let instanceName ;
74+ let firewallRuleName ;
75+ // 'this.externalIP' will be used to store the IP in the Mocha context
76+
4177 before ( async function ( ) {
42- this . timeout ( 200000 ) ;
43- cp . execSync (
44- `gcloud compute instances create my-app-instance-${ uniqueID } \
45- --image-family=debian-10 \
46- --image-project=debian-cloud \
47- --machine-type=g1-small \
48- --scopes userinfo-email,cloud-platform \
49- --metadata app-location=us-central1-f \
50- --metadata-from-file startup-script=gce/startup-script.sh \
51- --zone us-central1-f \
52- --tags http-server` ,
53- { cwd : path . join ( __dirname , '../../' ) }
54- ) ;
55- cp . execSync ( `gcloud compute firewall-rules create default-allow-http-8080-${ uniqueID } \
56- --allow tcp:8080 \
57- --source-ranges 0.0.0.0/0 \
58- --target-tags http-server \
59- --description "Allow port 8080 access to http-server"` ) ;
78+ this . timeout ( 240000 ) ; // Timeout for the before hook (e.g., 4 minutes)
79+ console . time ( 'beforeHookDuration' ) ;
6080
61- try {
62- const timeOutPromise = new Promise ( ( resolve , reject ) => {
63- setTimeout ( ( ) => reject ( 'Timed out!' ) , 90000 ) ;
64- } ) ;
65- await Promise . race ( [ timeOutPromise , getIP ( uniqueID ) ] ) ;
66- } catch ( err ) {
67- testFlag = false ;
68- }
69- console . timeEnd ( 'beforeHook' ) ;
70- } ) ;
81+ uniqueID = uuidv4 ( ) . split ( '-' ) [ 0 ] ;
82+ instanceName = `my-app-instance-${ uniqueID } ` ;
83+ firewallRuleName = `default-allow-http-${ APP_PORT } -${ uniqueID } ` ;
7184
72- after ( function ( ) {
85+ console . log ( `Creating GCE instance: ${ instanceName } ` ) ;
7386 try {
7487 cp . execSync (
75- `gcloud compute instances delete my-app-instance-${ uniqueID } --zone=us-central1-f --delete-disks=all`
88+ `gcloud compute instances create ${ instanceName } \
89+ --image-family=${ IMAGE_FAMILY } \
90+ --image-project=${ IMAGE_PROJECT } \
91+ --machine-type=${ MACHINE_TYPE } \
92+ --scopes userinfo-email,cloud-platform \
93+ --metadata app-location=${ GCP_ZONE } \
94+ --metadata-from-file startup-script=${ STARTUP_SCRIPT_PATH } \
95+ --zone ${ GCP_ZONE } \
96+ --tags http-server` , // Keep a generic tag if startup script handles specific app setup
97+ { cwd : path . join ( __dirname , '../../' ) , stdio : 'inherit' } // Show gcloud output
7698 ) ;
99+ console . log ( `Instance ${ instanceName } created.` ) ;
100+
101+ console . log ( `Creating firewall rule: ${ firewallRuleName } ` ) ;
102+ cp . execSync (
103+ `gcloud compute firewall-rules create ${ firewallRuleName } \
104+ --allow tcp:${ APP_PORT } \
105+ --source-ranges 0.0.0.0/0 \
106+ --target-tags http-server \
107+ --description "Allow port ${ APP_PORT } access for ${ instanceName } "` ,
108+ { stdio : 'inherit' }
109+ ) ;
110+ console . log ( `Firewall rule ${ firewallRuleName } created.` ) ;
111+
112+ console . log ( 'Attempting to get external IP...' ) ;
113+ this . externalIP = await getExternalIP ( instanceName , GCP_ZONE ) ;
114+ console . log ( `Instance IP: ${ this . externalIP } ` ) ;
115+
116+ const appAddress = `http://${ this . externalIP } :${ APP_PORT } /` ;
117+ console . log ( `Pinging application at ${ appAddress } ...` ) ;
118+ await pingVMExponential ( appAddress ) ; // pingVMExponential will throw on failure
119+
120+ console . log ( 'Setup complete.' ) ;
77121 } catch ( err ) {
78- console . log ( "wasn't able to delete the instance" ) ;
122+ console . error ( 'Error in "before" hook:' , err . message ) ;
123+ throw err ; // Re-throw to make Mocha mark 'before' as failed
124+ } finally {
125+ console . timeEnd ( 'beforeHookDuration' ) ;
79126 }
80- console . timeEnd ( 'afterHook' ) ;
81127 } ) ;
82128
83- it ( 'should get the instance' , async ( ) => {
84- if ( testFlag ) {
85- console . log ( `http://${ externalIP } :8080/` ) ;
86- const response = await fetch ( `http://${ externalIP } :8080/` ) ;
87- const body = await response . text ( ) ;
88- expect ( body ) . to . include ( 'Hello, world!' ) ;
129+ after ( async function ( ) {
130+ // 'after' hooks run even if 'before' or tests fail.
131+ this . timeout ( 120000 ) ; // Timeout for cleanup (e.g., 2 minutes)
132+ console . time ( 'afterHookDuration' ) ;
133+ console . log ( 'Starting cleanup...' ) ;
134+
135+ await cleanupResources ( instanceName , firewallRuleName , GCP_ZONE , this . externalIP ) ;
136+
137+ console . timeEnd ( 'afterHookDuration' ) ;
138+ } ) ;
139+
140+ // Helper for cleanup to be used in 'after' and potentially in 'before' catch block
141+ async function cleanupResources ( instName , fwRuleName , zone , ip ) {
142+ if ( instName ) {
143+ try {
144+ console . log ( `Deleting GCE instance: ${ instName } ` ) ;
145+ cp . execSync (
146+ `gcloud compute instances delete ${ instName } --zone=${ zone } --delete-disks=all --quiet` ,
147+ { stdio : 'inherit' }
148+ ) ;
149+ console . log ( `Instance ${ instName } deleted.` ) ;
150+ } catch ( err ) {
151+ console . warn ( `Warning: Wasn't able to delete instance ${ instName } . Error: ${ err . message } ` ) ;
152+ console . warn ( "You may need to delete it manually." ) ;
153+ }
154+ }
155+
156+ if ( fwRuleName ) {
157+ try {
158+ console . log ( `Deleting firewall rule: ${ fwRuleName } ` ) ;
159+ cp . execSync ( `gcloud compute firewall-rules delete ${ fwRuleName } --quiet` , { stdio : 'inherit' } ) ;
160+ console . log ( `Firewall rule ${ fwRuleName } deleted.` ) ;
161+ } catch ( err ) {
162+ console . warn ( `Warning: Wasn't able to delete firewall rule ${ fwRuleName } . Error: ${ err . message } ` ) ;
163+ console . warn ( "You may need to delete it manually." ) ;
164+ }
89165 }
90- console . timeEnd ( 'test' ) ;
166+ // Optional: Release static IP if you were using one
167+ // if (ip && IS_STATIC_IP) { /* gcloud compute addresses delete ... */ }
168+ }
169+
170+ it ( 'should get the instance and verify content' , async function ( ) {
171+ this . timeout ( 30000 ) ; // Timeout for this specific test
172+ console . time ( 'testExecutionTime' ) ;
173+ expect ( this . externalIP , "External IP should be available" ) . to . exist ;
174+
175+ const appUrl = `http://${ this . externalIP } :${ APP_PORT } /` ;
176+ console . log ( `Testing application at: ${ appUrl } ` ) ;
177+
178+ const response = await fetch ( appUrl ) ;
179+ expect ( response . status , "Response status should be 200" ) . to . equal ( 200 ) ;
180+
181+ const body = await response . text ( ) ;
182+ expect ( body ) . to . include ( 'Hello, world!' ) ;
183+ console . log ( 'Test verification successful.' ) ;
184+ console . timeEnd ( 'testExecutionTime' ) ;
91185 } ) ;
92- } ) ;
186+
187+ } ) ;
0 commit comments