Skip to content

Commit a664c3b

Browse files
add tests
1 parent 40b00f5 commit a664c3b

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This file will be copied to the "analytics" folder so it will be executed as a subprocess.
2+
// This will allow checking that the subprocess received the correct payload.
3+
4+
// We need to directly use the node method "fs" instead of "writeJsonFile" because a TypeScript ESM file cannot be imported without compiling it first
5+
import { writeFileSync } from "node:fs";
6+
import path from "node:path";
7+
8+
const PATH_TO_RESULT_FILE = path.join(
9+
process.cwd(),
10+
"test",
11+
"fixture-projects",
12+
"cli",
13+
"telemetry",
14+
"analytics",
15+
"result.json",
16+
);
17+
18+
(() => {
19+
const stringifiedPayload = process.argv[2];
20+
21+
writeFileSync(PATH_TO_RESULT_FILE, JSON.stringify(stringifiedPayload));
22+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"client_id": "hardhat_telemetry_consent",
3+
"user_id": "hardhat_telemetry_consent",
4+
"user_properties": {},
5+
"events": [
6+
{
7+
"name": "TelemetryConsentResponse",
8+
"params": {
9+
"userConsent": "yes"
10+
}
11+
}
12+
]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { Payload } from "../../../../../src/internal/cli/telemetry/analytics/types.js";
2+
3+
import assert from "node:assert/strict";
4+
import path from "node:path";
5+
import { after, afterEach, before, beforeEach, describe, it } from "node:test";
6+
7+
import { getConfigDir } from "@ignored/hardhat-vnext-core/global-dir";
8+
import {
9+
copy,
10+
exists,
11+
readJsonFile,
12+
remove,
13+
writeJsonFile,
14+
} from "@ignored/hardhat-vnext-utils/fs";
15+
16+
import {
17+
sendTaskAnalytics,
18+
sendTelemetryConsentAnalytics,
19+
} from "../../../../../src/internal/cli/telemetry/analytics/analytics.js";
20+
import { getHardhatVersion } from "../../../../../src/internal/utils/package.js";
21+
22+
// The analytics logic uses a detached subprocess to send the payload via HTTP call.
23+
// We cannot test the HTTP call directly, but we can use a test subprocess to verify if the payload is correctly created.
24+
// This is possible because the analytics code attempts to execute a subprocess file of type 'JS'. JS files are only available after compilation.
25+
// During the tests, no JS file is available, so the expected subprocess does not exist. Therefore, we can copy a test subprocess file
26+
// to the expected location instead of the original one and check if it receives the correct payload.
27+
28+
const PATH_TO_FIXTURE = path.join(
29+
process.cwd(),
30+
"test",
31+
"fixture-projects",
32+
"cli",
33+
"telemetry",
34+
"analytics",
35+
);
36+
37+
const SOURCE_PATH_TEST_SUBPROCESS_FILE = path.join(
38+
PATH_TO_FIXTURE,
39+
"analytics-subprocess.js",
40+
);
41+
42+
const DEST_PATH_TEST_SUBPROCESS_FILE = path.join(
43+
process.cwd(),
44+
"src",
45+
"internal",
46+
"cli",
47+
"telemetry",
48+
"analytics",
49+
"analytics-subprocess.js",
50+
);
51+
52+
const RESULT_FILE_PATH = path.join(PATH_TO_FIXTURE, "result.json");
53+
54+
async function copyTestSubprocessFile() {
55+
await copy(SOURCE_PATH_TEST_SUBPROCESS_FILE, DEST_PATH_TEST_SUBPROCESS_FILE);
56+
}
57+
58+
async function removeTestSubprocessFile() {
59+
remove(DEST_PATH_TEST_SUBPROCESS_FILE);
60+
}
61+
62+
async function setTelemetryConsentFile(consent: boolean) {
63+
const configDir = await getConfigDir();
64+
const filePath = path.join(configDir, "telemetry-consent.json");
65+
await writeJsonFile(filePath, { consent });
66+
}
67+
68+
async function checkIfSubprocessWasExecuted() {
69+
// Checks if the subprocess was executed by waiting for a file to be created.
70+
// Uses an interval to periodically check for the file. If the file isn't found
71+
// within a specified number of attempts, an error is thrown, indicating a failure in subprocess execution.
72+
const MAX_COUNTER = 20;
73+
74+
return new Promise((resolve, reject) => {
75+
let counter = 0;
76+
77+
const intervalId = setInterval(async () => {
78+
counter++;
79+
80+
if (await exists(RESULT_FILE_PATH)) {
81+
clearInterval(intervalId);
82+
resolve(true);
83+
} else if (counter > MAX_COUNTER) {
84+
clearInterval(intervalId);
85+
reject("Subprocess was not executed in the expected time");
86+
}
87+
}, 100);
88+
});
89+
}
90+
91+
describe("analytics", () => {
92+
before(async () => {
93+
copyTestSubprocessFile();
94+
});
95+
96+
after(async () => {
97+
await removeTestSubprocessFile();
98+
});
99+
100+
beforeEach(async () => {
101+
await remove(RESULT_FILE_PATH);
102+
});
103+
104+
afterEach(async () => {
105+
await remove(RESULT_FILE_PATH);
106+
});
107+
108+
it("should create the correct payload for the telemetry consent", async () => {
109+
await sendTelemetryConsentAnalytics(true);
110+
111+
await checkIfSubprocessWasExecuted();
112+
113+
const result = JSON.parse(await readJsonFile(RESULT_FILE_PATH));
114+
115+
const expected = await readJsonFile(
116+
path.join(PATH_TO_FIXTURE, "telemetry-consent-payload.json"),
117+
);
118+
119+
assert.deepEqual(result, expected);
120+
});
121+
122+
describe("analytics payload", async () => {
123+
const ORIGINAL_PROCESS_ENV = { ...process };
124+
125+
before(() => {
126+
// Force Ci to not be detected as Ci so the test can run (Ci is blocked for analytics)
127+
delete process.env.GITHUB_ACTIONS;
128+
delete process.env.NOW;
129+
delete process.env.DEPLOYMENT_ID;
130+
delete process.env.CODEBUILD_BUILD_NUMBER;
131+
delete process.env.CI;
132+
delete process.env.CONTINUOUS_INTEGRATION;
133+
delete process.env.BUILD_NUMBER;
134+
delete process.env.RUN_ID;
135+
136+
process.stdout.isTTY = true;
137+
});
138+
139+
after(() => {
140+
process = ORIGINAL_PROCESS_ENV;
141+
});
142+
143+
it("should create the correct payload for the task analytics", async () => {
144+
await setTelemetryConsentFile(true);
145+
146+
const wasSent = await sendTaskAnalytics("hardhat", "compile");
147+
148+
await checkIfSubprocessWasExecuted();
149+
150+
const result: Payload = JSON.parse(await readJsonFile(RESULT_FILE_PATH));
151+
152+
assert.equal(wasSent, true);
153+
154+
// Check payload properties
155+
assert.notEqual(result.client_id, undefined);
156+
assert.notEqual(result.user_id, undefined);
157+
assert.equal(result.user_properties.projectId.value, "hardhat-project");
158+
assert.equal(result.user_properties.userType.value, "Developer");
159+
assert.equal(
160+
result.user_properties.hardhatVersion.value,
161+
await getHardhatVersion(),
162+
);
163+
assert.notEqual(result.user_properties.operatingSystem.value, undefined);
164+
assert.notEqual(result.user_properties.nodeVersion.value, undefined);
165+
assert.equal(result.events[0].name, "task");
166+
assert.equal(result.events[0].params.engagement_time_msec, "10000");
167+
assert.notEqual(result.events[0].params.session_id, undefined);
168+
assert.equal(result.events[0].params.task, "hardhat");
169+
assert.equal(result.events[0].params.scope, "compile");
170+
});
171+
});
172+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { AnalyticsFile } from "../../../../../src/internal/cli/telemetry/analytics/types.js";
2+
3+
import assert from "node:assert/strict";
4+
import os from "node:os";
5+
import path from "node:path";
6+
import { afterEach, beforeEach, describe, it } from "node:test";
7+
8+
import { getTelemetryDir } from "@ignored/hardhat-vnext-core/global-dir";
9+
import { isCi } from "@ignored/hardhat-vnext-utils/ci";
10+
import {
11+
readJsonFile,
12+
remove,
13+
writeJsonFile,
14+
} from "@ignored/hardhat-vnext-utils/fs";
15+
16+
import {
17+
getClientId,
18+
getUserType,
19+
} from "../../../../../src/internal/cli/telemetry/analytics/utils.js";
20+
21+
const ANALYTICS_FILE_NAME = "analytics.json";
22+
23+
type FileType = "current" | "firstLegacy" | "secondLegacy";
24+
25+
const CLIENT_ID_CURRENT = "from-current-file";
26+
const CLIENT_ID_FIRST_LEGACY = "from-first-legacy-file";
27+
const CLIENT_ID_SECOND_LEGACY = "from-second-legacy-file";
28+
29+
async function createClientIdFile(fileType: FileType) {
30+
const [filePath, clientId] = await getFileInfo(fileType);
31+
await writeJsonFile(filePath, {
32+
analytics: {
33+
clientId,
34+
},
35+
});
36+
}
37+
38+
async function getClientIdFromFile(fileType: FileType) {
39+
const data: AnalyticsFile = await readJsonFile(
40+
(await getFileInfo(fileType))[0],
41+
);
42+
43+
return data.analytics.clientId;
44+
}
45+
46+
async function deleteClientIdFile(fileType: FileType) {
47+
const [filePath] = await getFileInfo(fileType);
48+
await remove(filePath);
49+
}
50+
51+
async function getFileInfo(fileType: FileType) {
52+
let filePath: string;
53+
let clientId: string;
54+
55+
if (fileType === "current") {
56+
filePath = path.join(await getTelemetryDir(), ANALYTICS_FILE_NAME);
57+
clientId = CLIENT_ID_CURRENT;
58+
} else if (fileType === "firstLegacy") {
59+
filePath = path.join(os.homedir(), ".buidler", "config.json");
60+
clientId = CLIENT_ID_FIRST_LEGACY;
61+
} else {
62+
filePath = path.join(await getTelemetryDir("buidler"), ANALYTICS_FILE_NAME);
63+
clientId = CLIENT_ID_SECOND_LEGACY;
64+
}
65+
66+
return [filePath, clientId];
67+
}
68+
69+
describe("telemetry/analytics/utils", () => {
70+
describe("clientId", () => {
71+
beforeEach(async () => {
72+
await deleteClientIdFile("current");
73+
await deleteClientIdFile("firstLegacy");
74+
await deleteClientIdFile("secondLegacy");
75+
});
76+
77+
afterEach(async () => {
78+
await deleteClientIdFile("current");
79+
await deleteClientIdFile("firstLegacy");
80+
await deleteClientIdFile("secondLegacy");
81+
});
82+
83+
it("should set a new clientId because the value is not yet defined", async () => {
84+
const clientId = await getClientId();
85+
86+
// The clientId should be generate as uuid
87+
assert.notEqual(clientId, undefined);
88+
assert.notEqual(clientId, CLIENT_ID_CURRENT);
89+
assert.notEqual(clientId, CLIENT_ID_FIRST_LEGACY);
90+
assert.notEqual(clientId, CLIENT_ID_SECOND_LEGACY);
91+
92+
// The clientId should also be saved in the file
93+
assert.equal(clientId, await getClientIdFromFile("current"));
94+
});
95+
96+
it("should get the 'current' clientId because it already exists", async () => {
97+
await createClientIdFile("current");
98+
const clientId = await getClientId();
99+
assert.equal(clientId, CLIENT_ID_CURRENT);
100+
});
101+
102+
it("should get the 'firstLegacy' clientId because it already exists and store it the new analytics file (current)", async () => {
103+
await createClientIdFile("firstLegacy");
104+
105+
const clientId = await getClientId();
106+
assert.equal(clientId, CLIENT_ID_FIRST_LEGACY);
107+
assert.equal(clientId, await getClientIdFromFile("current"));
108+
});
109+
110+
it("should get the 'secondLegacy' clientId because it already exists and store it the new analytics file (current)", async () => {
111+
await createClientIdFile("secondLegacy");
112+
113+
const clientId = await getClientId();
114+
assert.equal(clientId, CLIENT_ID_SECOND_LEGACY);
115+
assert.equal(clientId, await getClientIdFromFile("current"));
116+
});
117+
});
118+
119+
it("should return the correct user type", () => {
120+
const userType = isCi() ? "CI" : "Developer";
121+
assert.equal(getUserType(), userType);
122+
});
123+
});

0 commit comments

Comments
 (0)