Skip to content

Commit 15f17e9

Browse files
committed
chore: refactor test code and update debian
1 parent d3e6f60 commit 15f17e9

File tree

1 file changed

+158
-63
lines changed

1 file changed

+158
-63
lines changed

gce/test/app.test.js

Lines changed: 158 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,187 @@
1+
12
const cp = require('child_process');
23
const path = require('path');
34
const fetch = require('node-fetch');
45
const {expect} = require('chai');
56
const {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

1018
async 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

Comments
 (0)